GitHubActionsでDanger JSを使用する

## 概要
- DangerとはPull Request(以下PR)の自動チェックを行えるツールです。
- 今回は`GitHubActions`を使って、PRを行ったときにDangerでそのPRの自動チェックを走らせてみます。

<img width="600" alt="image" src="https://i.imgur.com/zJsQ03w.png">

## GitHub
- [danger\-js\-demo](https://github.com/pommdau/danger-js-demo)
- PRの例は[\[WIP\] 大きなファイルを10以上追加 \#16](https://github.com/pommdau/danger-js-demo/pull/16)を参照。

## 参考
- Danger-js公式
  - https://danger.systems/js/
- [Danger \(JS\) を GitHub Actions で導入する](https://obel.hatenablog.jp/entry/20220412/1649703600)
- [Dangerでpull requestレビューの指摘事項を減らす](https://www.slideshare.net/ShunsukeMaeda/dangerpull-request)
- [iOSにおけるコードレビューを一歩先へ進める](https://www.slideshare.net/ShunsukeMaeda/ios-80347918)
- ブランチ名の取得
    - https://stackoverflow.com/questions/71219265/get-current-git-branch-name-in-danger-js-script

```js
// ブランチ名の取得例
warn("base: " + danger.github.pr.base.ref);  // develop
warn("head: " + danger.github.pr.head.ref);  // feature/hoge
```

- JSの動作確認用ページ(ローカルで.js用のエディタが無かったため)
    - https://codepen.io/pen/
- `secrets.GITHUB_TOKEN`の権限の話
    - [\[GitHub\]secrets\.GITHUB\_TOKENで実行できる権限について簡単ながらマッピングしてみた](https://dev.classmethod.jp/articles/mapping-secrets-github-token/)

## 導入例
- リポジトリに`./.github/workflows/actions.yml`を作成。
- GitHubActionsでPR時にDangerが走るように設定します。
- danger-js@xxxのバージョンは適宜置換します
    - https://github.com/danger/danger-js

```yml
name: Danger JS
on: [pull_request]

jobs:
  build:
    name: Danger JS
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Danger
      uses: danger/danger-js@11.0.2
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        DANGER_DISABLE_TRANSPILATION: true
```

- 続いてリポジトリ直下に`dangerfile.js`を作成します。
- 大半の設定はその他記事からの参考ですが、`checkBranchNames`の箇所は自分で書いてみました。jsは知らないので雰囲気ですが。

> - messageは単純にコメントを書き込むDSL。定型文を返す時など。
> - fail() を呼び出すと ci は失敗し、マージできないようになります。
> - warn() の場合は警告を表示しますが、マージは可能です。


```javascript
// ===== Functions =====

function checkBranchNames(head_branch, base_branch) {
  // head(例: feature) -> base(例: develop)
  const template_branches = ["release", "master", "main", "develop", "feature/", "revert/", "hotfix/"];

  // ブランチ名が想定する名称かどうかの確認
  is_valid_head_branch_name = false;
  for (const template_branch of template_branches) {
    if (head_branch.startsWith(template_branch)) {
      is_valid_head_branch_name = true;
      break;
    }
  }

  if (!is_valid_head_branch_name) {
    // warn(`ブランチ名が不正です: ${head_branch}\n次のいずれかの名称を使用してください: \n${template_branch_names.join(' ')}`);
    fail(`ブランチ名が不正です: ${head_branch}\n次のいずれかの名称を使用してください: \n${template_branches.join(' ')}`);
  }

  // ブランチ名とマージの整合性確認
  // 仮に誤ってdevelop -> featureとしてしまったときに検出させるとか。
  // TODO...
}

// ===== Entry Point =====


let isAllCheckPassed = true;

// ===== Title =====

// PRのタイトルに[WIP]が含まれる場合は、作業中なのでfailさせる
if (danger.github.pr.title.includes('[WIP]')) {
  fail("Should NOT inclued 'WIP' in your PR title");
}

// PRのタイトルに課題番号が含まれているかどうか(DEV_1-XXX)
const hasIssuesNumber = /DEV_1-[0-9]/.test(danger.github.pr.title);
if (!hasIssuesNumber) {
  warn("Should include issues number in your PR title");
  isAllCheckPassed = false;
}

// ===== Settings =====

// PRにレビュアーが指定されているかどうか
if (!danger.github.pr.reviews) {
  warn("Should select PR reviewer");
  isAllCheckPassed = false;
}

// PRにassigneesが指定されているかどうか
if (!danger.github.pr.assignee) {
  warn("Should select PR assignees");
  isAllCheckPassed = false;
}

// ===== Changing amount =====

// 500行以上の追加・削除の変更があったかどうか
const diffSize = Math.max(danger.github.pr.additions, danger.github.pr.deletions);
if (diffSize > 500) {
  warn("Should reduce diffs less than 500");
  isAllCheckPassed = false;
}

// 変更したファイルの数が10より多いかどうか
if (danger.github.pr.changed_files > 10 ) {
  warn("Should reduce change files less than 10");
  isAllCheckPassed = false;
}

// ===== Branch =====

checkBranchNames(danger.github.pr.head.ref, danger.github.pr.base.ref)

// ===== Result =====

if (isAllCheckPassed) {
  markdown('## All checkes have passed :tada:');
}
```

- 以上の内容をmainブランチにプッシュしてPRを作ってやると、GitHubActionsによってDanger JSが動作しPRの自動チェックが走ってくれます。

<img width="600" alt="image" src="https://i.imgur.com/zJsQ03w.png">

- PRを更新して再度チェックする場合は以下をクリック（多分）

<img width="400" alt="image" src="https://i.imgur.com/PTg8er5.png">

## その他チェック項目の例
- Dangerで実現できることを把握したかったので、他の方の事例で行っているをいくつか抜粋しています。

### [Danger \(JS\) を GitHub Actions で導入する](https://obel.hatenablog.jp/entry/20220412/1649703600)
> - CHANGELOG.md が変更されていないと怒られるやつです。

<details>
<summary>dangerfile.js</summary>

```js
const hasChangelog = danger.git.modified_files.includes('CHANGELOG.md')
const isTrivial = (danger.github.pr.body + danger.github.pr.title).includes(
  '#trivial'
)

if (!hasChangelog && !isTrivial) {
  warn('Please add a changelog entry for your changes.')
}
```

</details>

### [コードレビューの機械的な指摘はDangerに任せる](https://techblog.lclco.com/entry/2017/12/29/211627)

- また別リポジトリにあるDangerfileを参照することができる。
- 多分こういうことですね。
  - https://github.com/capistrano/capistrano/blob/master/Dangerfile
  - https://github.com/capistrano/danger/blob/master/Dangerfile

> 各リポジトリでDangerfileを用意してチェック項目を記述していますが、組織独自のルールなど共通する処理は専用リポジトリで管理して参照するようにしています。
> 例えば、danger-lclというリポジトリを用意して共通のチェック項目を記述したDangerfileを管理します。 そして別のリポジトリのDangerfileで下記を追加することで、そのリポジトリのDangerfileの内容を読み込むことができます。

```js
# dangerfile.js内
# danger-lclを読み込み
danger.import_dangerfile(github: "XXX/danger-lcl")
```

<details>
<summary>dangerfile.js</summary>

```js
# ===== Description =====

# 本文が1行以上書かれているか（10行以下の軽微な変更は考慮）
warn "作業内容について本文に1行以上の説明を記載してください。" if github.pr_body.length < 1 && git.lines_of_code > 10

...

# ===== Label =====

labels = github.pr_labels

# ラベルが付いていなかったら「wip」ラベルを付ける
add_label "wip", 'fbca04' if labels.empty?
```

</details>

### [Dangerで始めるPull Requestチェック自動化](https://tech.connehito.com/entry/danger)

- ライブラリの更新があった場合にテストを書くことをチェック
- ブランチの運用確認

<details>
<summary>dangerfile.js</summary>

```js
has_lib_changes = !git.modified_files.grep(/^lib/).empty?
has_test_changes = !git.modified_files.grep(/^(features|spec)/).empty?
has_changelog_changes = git.modified_files.include?("CHANGELOG.md")
if has_lib_changes && !has_test_changes
  warn("There are code changes, but no corresponding tests. "\
       "Please include tests if this PR introduces any modifications in "\
       "#{project_name}'s behavior.",
       :sticky => false)
end
```

```js
# --------------------
# base branch
# --------------------
is_to_master = github.branch_for_base == 'master'
is_to_release = !!github.branch_for_base.match(/release-[0-9]+\.[0-9]+\.[0-9]/)
is_from_release = !!github.branch_for_head.match(/release-[0-9]+\.[0-9]+\.[0-9]/)

if is_to_master && !is_from_release
  warn('masterへmerge出来るのはrelease branchのみです。')
end

if is_to_release
  warn('release branchに対してPRを向けないで下さい。develop branchに向けてPRを作成し、develop branchをrelease branchにmergeしてください。')
end
```

</details>

### [アプリ開発を支える10個のDangerレシピ](https://tech.mirrativ.stream/entry/2022/04/28/105807)

<details>
<summary>dangerfile.js</summary>

> - githubの更新があった箇所のみ指摘する

```js
github.dismiss_out_of_range_messages
```

> - PR担当者を該当PRの起票者に割り当てたい

```js
## assigneeが未割り当てかのチェック
if github.pr_json['assignee'] == nil
  ## github.api 経由で GitHubの操作可能
  github.api.add_assignees(
    github.pr_json['base']['repo']['full_name'],
    github.pr_json['number'],
    [github.pr_author] ## <- 起票者は github.pr_author
  )
end
```

>レシピ４：JIRAのリンクを掲載したい

```js
git.commits.each do |commit|
  # コミットメッセージからJIRAの番号を抜き出しています。"MIR-"の部分は自社でお使いのJIRA IDに置き換えてください。
  match = commit.message.match /(MIR-\d+)/ 
  if match
    message(
      ":paperclip: <a href='https://xxx.atlassian.net/browse/#{match[1]}'>#{match[1]}</a>"
    )
  end
end
```

</details>

## 所感
- PRのメッセージのテンプレートを設定することに加えて、自動チェックを走らせられることでより快適にPR運用ができる。
- GitHubActions自体に言えることですが、リポジトリのプログラムとは独立して設定できるのは気楽で良いですね。
- Dangerfileの言語にはRuby,TypeScript,JavaScriptといろいろ使えるので、ネットで検索する際は混同しないように注意。
- `actions.yml`、`dangerfile.js`の設定はデフォルトブランチのみ有効になります。
    - そのためDangerの設定でいろいろ試す際は別のリポジトリで試したり、(お行儀は良くないですが)mainブランチでrebaseしてコミット履歴をきれいにする、もしくはローカルでDangerを動かすといいかなと思っています。