【GitHub Actions】Markdown 執筆に textlintの自動校正を取り入れる

2022.05.02

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

背景

GitHub で Markdown 執筆活動

複数人で協力してドキュメントを執筆しています。 執筆の流れは下図のとおり。

img

  1. 執筆ブランチ を作成する
  2. 執筆ブランチ で執筆活動を行う
  3. mainブランチ にマージするための プルリクエスト を作成する
  4. プルリクエスト内でレビュー (必要に応じて修正) を行う
  5. 最後に mainブランチ へマージする (1. に戻る)

最低限 Git や GitHub 周りの操作を把握できたら、この方法は結構良いと感じています。 以下のようなメリットを享受できています。

  • テキストで情報を管理できる ( by Markdown )
  • バージョン管理ができる( by Git )
  • 文章レビューの流れを統一できる( by GitHub )

textlint で文章校正

さて、執筆活動において より良い文章 を書くために textlint を活用しています。 textlint は 文章校正ツールです。Node.js のパッケージとして提供されています。 公開されている様々なルールを適用してカスタマイズ可能、 「良い文章を書くためのガードレール」として活躍します。

※ textlint についての詳細は以下記事がとても分かりやすいので、御覧ください。

今までは、この textlintを執筆者のローカルPCに導入して、各自使うようにしていました。

やりたいこと

textlint による校正プロセスを自動化 します。 下図のように textlintチェックを プルリクエスト内に統合 させようと思います。

img

トリガーは「プルリクエスト作成時」および 「その作業ブランチ更新時」です。 そのタイミングで textlint による校正を実行する GitHub Actions ワークフローを実装します。

とりあえず動くモノが欲しい人向け

以下リポジトリに実際に動くモノを載せています。適宜複製して活用ください。

▼ プルリクエスト時の Botコメント例

img

作ってみる(最小実装版)

#1: textlint 環境の実装

先にローカル環境で textlint実行環境 を構築します。 構築の情報 ( package.json など)を次ステップの 「ワークフローの実装」で活用します。

前提

Node.js および npmの実行環境構築は省略します。以下バージョンを使っています。

node --version
# --> v14.19.1
npm --version
# --> 6.14.16

Volta というパッケージ管理ツールが便利でした。

npm install で各種パッケージをインストール

npm init で初期化してから、 以下コマンドで必要なパッケージをインストールします。

npm install --save-dev \
  textlint \
  textlint-filter-rule-allowlist \
  textlint-rule-preset-ja-technical-writing

インストールしたものは以下のとおりです。 ※ルールセットは他にも色々あるので、適宜カスタマイズしてみてください。

この時点で、 package.jsondevDependencies キーにインストールしたパッケージ情報が記載されているはずです。

package.json

{
  (..略...)
  "devDependencies": {
    "textlint": "^12.1.1",
    "textlint-filter-rule-allowlist": "^4.0.0",
    "textlint-rule-preset-ja-technical-writing": "^7.0.0"
  }
}

package.json および package-lock.json を GitHubリポジトリへプッシュしておきます。

textlint の初期設定

.textlintrc を作成します。これは textlint の設定ファイルです。 (書き方のルール詳細は割愛します)

.textlintrc

{
  "rules": {
    "preset-ja-technical-writing": true
  },
  "filters": {
    "allowlist": {
      "allow": [
        "ここに許容される言葉を書いていく"
      ]
    }
  }
}

この状態で textlint がローカルで実行できることを確かめます。

echo "テストかもしれない" > ./test.md                                                                 main
npx textlint ./test.md
# ${PWD}/test.md
#   1:4  error  弱い表現: "かも" が使われています。  ja-technical-writing/ja-no-weak-phrase
#   1:9  error  文末が"。"で終わっていません。       ja-technical-writing/ja-no-mixed-period
#  
# ✖ 2 problems (2 errors, 0 warnings)

.textlintrc を GitHubリポジトリへプッシュしておきます。

#2: ワークフローの実装

.github/workflows/run-textlint-minimum.yml を作成します。

.github/workflows/run-textlint-minimum.yml

name: run-textlint-minimum
on: 
  pull_request_target:
    types: [ opened, synchronize ]
    paths: [ 'contents/**/*.md' ]
jobs:
  run-textlint-minimum:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - uses: actions/setup-node@v3
        with:
          node-version: 14
      - run: npm install
      - run: npx textlint ./contents/**/*.md >> ./.textlint.log
      - if: ${{ failure() }}
        run: gh pr comment --body-file ./.textlint.log "${URL}"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          URL: ${{ github.event.pull_request.html_url }}

前半部分は下図にざっくり解説を書きました。

img

各ステップ( steps: ) の内容を解説していきます。

ブランチ切り替え (actions/checkout@v3)

- uses: actions/checkout@v3
  with:
    ref: ${{ github.event.pull_request.head.sha }}

actions/checkout@v3 を使ってブランチを切り替えます。 今回はレビューを行う作業ブランチに切り替えたいので、 パラメータを指定します。 ( この example どおりに ref: $... 部分を書いています )

Node.jsセットアップ (actions/setup-node@v3)

- uses: actions/setup-node@v3
  with:
    node-version: 14

actions/setup-node@v3 を使って Node.js 環境をセットアップします。 パラメータとして「先程のローカル環境の Node.jsのバージョン(14)」を指定しています。

パッケージインストール (npm install)

- run: npm install

このコマンドで、 package.json 記載の Node.js パッケージをインストールします。

textlint実行 (npx textlint ...)

- run: npx textlint ./contents/**/*.md >> ./.textlint.log

.textlintrc の設定で textlint を実行します。 contents ディレクトリ配下の mdファイルがチェック対象です。 実行ログを .textlint.log に格納します。

校正内容をプルリクエスト上にコメント (gh pr comment ...)

- if: ${{ failure() }}
  run: gh pr comment --body-file ./.textlint.log "${URL}"
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    URL: ${{ github.event.pull_request.html_url }}

前ステップで校正ルールに引っかかったときは、 そのステップは失敗扱いになります 。 そのため「失敗したときのみ実行する」条件を記載しています ( if: ${{ failure() }} 部分)。

コメント送信は GitHub CLI : gh を使って実現しています。 gh コマンドは ubuntu-latest Runner にデフォルトで入っています。 gh コマンドで必要な情報、つまり「プルリクエストのURL」と「GitHubアクセスの認証情報」を 環境変数 (env:)に指定します。

[2023-08-10 追記]

textlint側の設定 で エラーにならない(status=0)ように設定できます。それを使ってエラー回避するのが良さそうです。

確認する

以下コマンドを流して、リモートリポジトリへ作業ブランチをプッシュします。

# 作業ブランチの作成
git branch --create write-xxx-section

# 作業ブランチに移動
git switch write-xxx-section

# 執筆活動
echo "# テスト\nこれはテストかもしれない。\nこんにちは" > ./contents/XX_xxx.md

# 変更をコミット
git add ./contents/XX_xxx.md
git commit -m "xxx章記載"

# リモートリポジトリへプッシュ
git push origin write-xxx-section

GitHubリポジトリ上でプルリクエストを作成してみます。

img

しばらくすると以下のようなコメントが飛んできました。

img

指摘を受けてファイルを修正、再度プッシュします。

img

…チェックに合格したので指摘コメントは飛んできませんでした (めでたく良い文章になりました)。

ワークフローを改善する

行数は増えますが、最小実装版からいくつか改善したものを作りました。

.github/workflows/run-textlint.yml

name: run-textlint
on: 
  pull_request_target:
    types: [ opened, synchronize ]
    paths: [ 'contents/**/*.md' ]
jobs:
  run-textlint:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - name: Switch to pull request branch
        uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - name: Setup node with cache
        uses: actions/setup-node@v3
        with:
          node-version: 14
          cache: 'npm'
      - name: Install packages via packages.json
        run: npm install
      - name: Run textlint (avoiding error)
        run: npx textlint ./contents/**/*.md -o ./.textlint.log | true
        shell: bash {0}
      - name: Report if textlint finds problems
        run: |
          if [ -e ./.textlint.log ]; then
            # create body file
            pwd_esc=$(pwd | sed 's/\//\\\//g')
            cat ./.textlint.log | sed "s/${pwd_esc}/### :policeman: ./g" >> ./.body.txt
            # pr comment
            gh pr comment --body-file ./.body.txt "${URL}"
          fi        
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          URL: ${{ github.event.pull_request.html_url }}

以降で改善した点を書いていきます。

キャッシュの利用

- name: Setup node with cache
  uses: actions/setup-node@v3
  with:
    node-version: 14
    cache: 'npm'

actions/setup-nodeCaching npm dependencies: を参考にしました。 依存関係をキャッシュして実行時間を減らす試みです。

(ジョブのログを見ると、たしかにキャッシュを活用しているように見受けられます。 が、そこまで全体として大きく実行時間が減った感じはしませんでした…。 もっと時間削減できる工夫があればご指摘ください!)

img

エラーの回避

- name: Run textlint (avoiding error)
  run: npx textlint ./contents/**/*.md -o ./.textlint.log | true
  shell: bash {0}

一言でいうと コマンドでエラーを出さない ようにしています。 この対応を行う理由は 『setup/node のキャッシュ仕様』にあります。 このステップでエラーになると 『setup/node はキャッシュ保存をしてくれない』ためです。

▼ run 部分の解説

まず、前提として textlint は指摘があった際はエラー (終了ステータスが 0 以外)となります。

# ### 指摘が無い場合
echo "こんにちは。" | npx textlint --stdin -f compact
# --> (出力無し)

echo $?
# --> 0

# ### 指摘がある場合
echo "こんにちは" | npx textlint --stdin -f compact
# <text>: line 1, col 5, Error - 文末が"。"で終わっていません。 (ja-technical-writing/ja-no-mixed-period)
#  
# 1 problem

echo $?
# --> 1

npx textlint ... | true とすることで textlint の指摘の有無に関わらず 終了ステータスを 0 とします。

echo "こんにちは" | npx textlint --stdin -f compact | true
echo $?
# --> 0

▼ shell 部分の解説

先程の終了ステータスをいじるだけでは、このステップは成功状態になりません。 GitHub Actions デフォルトの bash は -e および -o pipefail オプションが付いた状態で実行されるためです。 -e オプションは 『エラーが出たところでスクリプトを中断する』ことを意味します。 -o pipefail オプション は『パイプラインのどこかで失敗した場合は、 コマンド全体を失敗とする 』ものです。

-o pipefail があるため、 npx textlint ... | true と書いていても 終了ステータスが 0 とは限りません。

なので 「GitHub Actions デフォルトの bash」 を使わないようにします。 その設定が shell: bash {0} です。

出力の体裁調整

- name: Report if textlint finds problems
  run: |
    if [ -e ./.textlint.log ]; then
      # create body file
      pwd_esc=$(pwd | sed 's/\//\\\//g')
      cat ./.textlint.log | sed "s/${pwd_esc}/### :policeman: ./g" >> ./.body.txt
      # pr comment
      gh pr comment --body-file ./.body.txt "${URL}"
    fi        
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    URL: ${{ github.event.pull_request.html_url }}

これは調整後の出力を見ていただければ把握できると思います。 以下のような出力にしました。

img

ファイルパスを必要最小限 (=ホームディレクトリは削除) にしています。 さらに警察官絵文字を書いてアクセントを加えてみました。

警察官絵文字は置いておいて、多少は見やすくなったと思います。

おわりに

これまでの実装は以下リポジトリにまとめています。 適宜複製して活用ください。

参考