Asciidocで仕様書の「開発環境」を整える (2)

## 概要

モダンな仕様書の「開発環境」を構築した。
結果として、以下のような雛形の環境が出来上がった。

https://github.com/ynitto/asciidoc-template

## 前回

[前回](https://qiita.com/ynitto/items/fa44aae90c927e52613c)までで個々人のローカルで仕様書を書く準備は整った。

今回はチーム開発や運用効率化を行うべく、CI/CD を整備していく。

## 前提

前回に加えて、仕様書の CI/CD 環境を整える上で事前に用意しておくものは以下の通り。

- GitHub

GitLab や AWS CodeCommit などでもよい。その場合、以降の自動化の方法が異なってくるだろうが、やっていることは Linux でのコマンド実行なので、同様のことができる。

## 仕様書を CI する

### 仕様書を Pull Request ベース開発する

コーディングの世界では、チーム開発をスムーズに回すために Pull Request ベース開発という考え方がある。ざっくり言ってしまうと、コードのレビュー依頼 (Pull Request) をして、レビューOKで採用するというやり方。これをシステマチックに行う方法のことである。

https://qiita.com/harada4atsushi/items/527d5f98320d993b3072

今回は Git Flow を採用して以下の流れで開発することを想定する。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f35333330392f30363134303132312d613062362d343237662d633134392d3638353863313439373338652e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2784299/dc199ed0-920f-a2ca-fb70-98093c3707e9.png)

1. メンバーは`develop`から`feature/A`ブランチを生やす
1. メンバーは担当部分を追記して`feature/A`ブランチを push し、`develop`ブランチへの pull request を作る
1. リーダーが pull request を確認してレビューする。指摘事項はコメントで残す。
1. リーダーがレビュー完了 (指摘事項が対応済み) を確認したらマージする

メンバーやリーダーと書いたが、チームによって変わると思う。ただ、文章のレビューは、書き方の統一性を保つためにも誰か責任者を置いたほうがよい。または、予め書き方の方針を示し合わせた上で、複数人によるレビューを行なってもよい。

### Pull Request を自動でチェックする

仕様書のレビューは、誤解ない良いドキュメントを作る上で重要な作業だと思う。人手によるレビューは、より高度な文章の校正に注力したい。そのためにも、Typo などの簡単な問題は自動でレビューしたいものだ。そこで、Pull Request が生成された時に textlint を自動で走らせてコメントするように設定する。

textlint と言うと、前回ローカルでライブチェックする設定を導入したので、いらないのではと思われるかもしれない。しかし、実際に運用すると、textlint の指摘を修正せずに Pull Request が上げられることが、結構ある。マシンスペックによって、ライブチェックを OFF にしている、焦って確認せずに上げるなど理由は様々だが、いずれにしても複数回チェックすることには意味はある。

今回は GitHub を前提としているため、Pull Request による自動動作には GitHub Actions を使用した。
以下が今回作成したワークフローファイル。 (+`npm scripts`の追加)

```yaml:.github/workflows/run-lint.yml
name: run-lint
on:
  pull_request_target:
    types: [ opened, synchronize ]
    paths: [ 'src/**/*.adoc' ]
jobs:
  lint:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - name: Setup reviewdog
        uses: reviewdog/action-setup@v1
        with:
          reviewdog_version: latest
      - 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: 16
          cache: 'npm'
      - name: Install packages via packages.json
        run: npm install
      - name: Run lint
        run: npm run lint:ci
      - name: Report if textlint finds problems
        if: failure()
        run: |
          if [ -e ./.textlint.xml ]; then
            # report by reviewdog
            cat ./.textlint.xml | reviewdog -f=checkstyle -name="textlint" -reporter="github-pr-review" -level="warning"
            # clean output file
            rm -rf ./.textlint.xml
          fi
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          URL: ${{ github.event.pull_request.html_url }}
          REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

```json:package.json
{
...
  "scripts": {
    ...
    "lint:ci": "textlint ./src/**/*.adoc -f checkstyle -o ./.textlint.xml",
    ...
  },
...
}
```

やってることの流れとしては、

- すべての Pull Request に対して adoc ファイルに変更があった時のみ実行する
- textlint は`npm scripts`で実行する。指摘事項を検出するとエラーとなり、Pull Request に×印が付く。
- textlint の指摘事項がある場合 reviewdog を使って Pull Request のコメントとして表示する

もし、textlint は失敗しているが Pull Request のコメントが何もない場合は、Pull Request で追加・修正した場所ではないところが指摘されている可能性がある。その場合、この Pull Request で修正するように促そう。

なお、実際に動いている Pull Request の結果は[ここ](https://github.com/ynitto/asciidoc-template/pull/3)で確認できる。

## 仕様書を CD する

ここからは仕様書のリリースや管理など CD について構築する。

### 仕様書を Pull Request ベースでリリースする

まず、仕様書リリース時のフローをまとめる。今回も Git Flow を想定すると以下の流れになる。

1. リーダーは仕様書をリリースするタイミングで`develop`から`release/x.y.z`ブランチを生やす
1. リーダーやメンバーは`release/x.y.z`ブランチに対して、記載不備の修正など校正する。このとき仕様書のバージョンを上げる。
1. リーダーは`release/x.y.z`ブランチを push し、`develop`ブランチへの pull request を作る
1. リーダーは pull request を確認してマージする
1. リーダーは`release/x.y.z`ブランチを push し、`main`ブランチへの pull request を作る
1. リーダーは pull request を確認してマージする

こちらもリーダーである必要はない。リリースに責任を持って丁寧にやれれば、誰でもOKだと思う。

### 仕様書を自動でデプロイする

ここでは、仕様書を製本(HTML や PDF化)して、誰かと共有できる状態にすることを **デプロイ** と呼ぶことにする。
このデプロイ、仕様書の Continuous Delivery（継続的デリバリー） には二種類のタイミングがある。

- 開発中である程度追記が完了したタイミング
今回のフローでは`feature/A`ブランチが`develop`にマージされたタイミング。製本した成果物は中間データのため、バージョン管理する必要はない。現在の最新の状態が見れればよい。
- 正式版としてリリースするタイミング
今回のフローでは`release/x.y.z`ブランチが`main`にマージされたタイミング。製本した成果物はバージョン管理していつでも見返せる状態にしたい。

今回も Pull Request の自動チェックと同様、GitHub Actions を使用してこれらを実現する。
以下が今回作成したワークフローファイル。

```yaml:.github/workflows/run-build.yml
name: run-build
on:
  pull_request:
    branches: [ develop, main ]
    types: [ closed ]
jobs:
  build:
    runs-on: ubuntu-latest
    if: github.event.pull_request.merged == true
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: Setup node with cache
        uses: actions/setup-node@v3
        with:
          node-version: 16
          cache: 'npm'
      - name: Install packages via packages.json
        run: npm install
      - name: Build Asciidoc
        run: npm run build
      - name: Publish latest
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist
          keep_files: true
      - name: Create tag
        id: create-tag
        if: github.base_ref == 'main' && startsWith(github.head_ref, 'release')
        run: |
          TAG_NAME=$(echo ${{ github.head_ref }} | sed 's/release\///')
          echo "::set-output name=tag-name::${TAG_NAME}"
          echo $TAG_NAME
      - name: Publish versioned
        uses: peaceiris/actions-gh-pages@v3
        if: github.base_ref == 'main' && startsWith(github.head_ref, 'release')
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist
          destination_dir: ${{ steps.create-tag.outputs.tag-name }}
          keep_files: true
      - name: Create release
        if: github.base_ref == 'main' && startsWith(github.head_ref, 'release')
        uses: softprops/action-gh-release@v1
        with:
          name: ${{ steps.create-tag.outputs.tag-name }}
          tag_name: ${{ steps.create-tag.outputs.tag-name }}
          generate_release_notes: true
          files: dist/index.pdf
          draft: false
          prerelease: false
```

やってることの流れとしては、

- `develop`または`main`ブランチへ Pull Request がマージされた時のみ実行する
- `npm scripts`を実行して HTML と PDF ファイルを作る
- 作成した HTML と PDF を GitHub Pages のルートにデプロイする
- もし`release/x.y.z`ブランチを`main`にマージしたのであれば
    - 作成した HTML と PDF を GitHub Pages の`./x.y.z`以下にデプロイする
    - 作成した HTML と PDF を GitHub の Releases にアップロードする
    - `main`ブランチにタグ`x.y.z`を打つ

このワークフローによって、マージされた Pull Request の最新結果が常に配信されている状態となり、過去リリースした成果物はバージョン管理され配信かつダウンロード可能な状態を保つことができる。これによって、今までの「ネットワークフォルダ＋ファイル名管理」によるツラさや問題から脱却できる。

今回、`peaceiris/actions-gh-pages`を用いた GitHub Pages への公開を使ったため`gh-pages`ブランチが自動で生成される。初めて生成した後にリポジトリの`Settings > Pages`から`Build and deployment`の`Branch`を`gh-pages`に設定する必要がある。

なお、実際に動いている Pull Request の結果は[ここ](https://github.com/ynitto/asciidoc-template/actions/runs/2915368438)で確認できる。

## 最後に

二回に渡って仕様書をテーマに、調査と検証をしながら、「開発環境」を構築した。まだ「既存のやり方」との違いはあると思う。反発される点もまだあるだろう。しかし、冒頭に書いたように、この記事をきっかけに、やってみようと思った人が少しでも増えたら嬉しい。

なお、間違いなどあれば、是非ご指摘ください。また、「既存のやり方で出来る◯◯ができない」といった反発もウェルカムです。鋭意検討してアップデートしますので。