GitHub Actions を使って Vim をデイリービルドする

GitHub Actions を使って Windows 向けに Vim をデイリービルドする環境を構築したのですが、その際に得られた知見を残しておこうと思います。

今回作成したリポジトリは以下になります。

* [vim-kt](https://github.com/k-takata/vim-kt)

# はじめに

Windows 向けの Vim のバイナリパッケージは、既に以下のようなものがあります。

* [公式パッケージ][1]
* [vim-win32-installer][2]: 公式ナイトリービルド
* [香り屋 Vim][3]: koron氏による、日本語をより使いやすくするための変更を加えたパッケージ
* [tux氏によるパッケージ][4]

[1]: https://www.vim.org/download.php#pc
[2]: https://github.com/vim/vim-win32-installer
[3]: https://github.com/koron/vim-kaoriya
[4]: https://tuxproject.de/projects/vim/

ただ、いずれのパッケージにも何らかの不満があったので、Vim 8.2 のリリースを機に自分でパッケージを作ってみることにしました。
パッケージの作成に当たっては、以下のような方針とすることにしました。

* 毎日自動でビルドする。
* [自分のパッチ](https://osdn.net/users/k_takata/pf/vim-ktakata-mq/wiki/FrontPage)を当てた状態でビルドする。
* いくつかの日本語関連のプラグインを同梱する。
* シンプルなディレクトリ構成にし、アーカイブ内のディレクトリ名にはバージョン番号を入れない。
  (バージョンが変わったときにもアーカイブファイルを展開するだけで簡単に更新できるようにする。)

# AppVeyor vs. GitHub Actions

今までは Windows 向けの CI 環境としては AppVeyor がよく使われていました。vim-win32-installer でも AppVeyor を使っていますが、GitHub Actions とどちらがよいか比較してみました。

* AppVeyor

  * Visual C++  
    Express 2008 から Community 2019 まで各種バージョンが使用可能。
  * MSYS2 や Cygwin もインストール済み。
  * 並列数: 1 (無料版)
  * スケジュール実行は可能だが、リポジトリごとに申請が必要。
  * シェルは PowerShell と cmd.exe が使用可能。

* GitHub Actions

  * Visual C++  
    windows-latest イメージの場合、Enterprise 2019 のみ。(実際には 2015 (エディション不明)もインストールされている模様。)
  * MSYS2 や Cygwin は未インストール。(追記: 2020 年 7 月ごろからは MSYS2 もインストール済み)
  * 並列数: 20
  * スケジュール実行可能。
  * シェルは PowerShell と cmd.exe 以外に、Git for Windows の bash が使用可能。さらにカスタムのシェルを使用することも可能。
  * 公式または第三者が用意したアクションという処理の単位を再利用可能。
  * GitHub との統合
    * 実行結果を GitHub の画面上から確認可能。
    * 認証用のトークンが設定済み。
  * リリースされたばかりなので、今後仕様変更の可能性もあり？

プリインストールされているソフトは AppVeyor の方が多いようですが、機能的には GitHub Actions の方が上のようなので、今回は GitHub Actions を試してみることにしました。

# GitHub Actions を使ってみる

基本的には AppVeyor 用に書かれた [vim-win32-installer][2] のスクリプトを参考に、GitHub Actions 用に書き直して、自分用の設定を追加する形で作業を進めました。ただ、AppVeyor と GitHub Actions の仕様の違いから、いくつかそのまま移植できないものがありました。

## ワークフローから新しいワークフローは実行できない

[vim-win32-installer][2] や、それを元にした [ctags-win32](https://github.com/universal-ctags/ctags-win32)、[the\_silver\_searcher-win32](https://github.com/k-takata/the_silver_searcher-win32) ではいずれも以下のような流れでデイリービルドを行っています。

1. リポジトリ内の `scripts/update-repo.sh` を 1 日 1 回実行。
   サブモジュールに新しいコミットがあれば、サブモジュールを更新し、コミットしてタグをプッシュ。
2. タグがプッシュされると、それをトリガーにして 64bit 版をビルド・テストし、成功すれば、新しい GitHub Release を作成してファイルをアップロード。続いて 32bit 版をビルド・テストし、成功すれば、同じ GitHub Release にファイルをアップロード。

これをそのまま別々のワークフローとして GitHub Actions に移植したところ、最初のワークフローでタグがプッシュされたにもかかわらず、次のワークフローが実行されませんでした。

公式ドキュメントの「[ワークフローをトリガーするイベント](https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows)」を見たところ、

> 実行しているワークフローのアクションからは、新しいワークフローの実行をトリガーできません。

と書かれていたことから、どうやってもこの構成では目的を達成できそうにありません。仕方ないのでワークフローを分割するのは諦めて、1 つのワークフローで、サブモジュールの更新のチェック、ビルド、テスト、パッケージの作成、タグのプッシュ、GitHub Release へのファイルのアップロードのすべてを実行するように構成を変更しました。

(上記ページに書かれている、外部イベント `repository_dispatch` を使えば、ワークフローからワークフローを実行することも可能かもしれないが、未確認。)

## 別々のジョブから GitHub Release にファイルをアップロードできない

前述の [vim-win32-installer][2] などでは、64bit 版をビルドするジョブと 32bit 版をビルドするジョブから、それぞれ GitHub Release を作成し、ファイルをアップロードしています。(実際には 32bit 版のジョブの実行時には既に同名の GitHub Release が作成されているため、そこにファイルが追加されます。)

GitHub Actions では、GitHub Release を作成するアクション ([actions/create-release](https://github.com/actions/create-release)) と、ファイルをアップロードするアクション ([actions/upload-release-asset](https://github.com/actions/upload-release-asset)) が分かれています。
困ったことに、actions/create-release で GitHub Release を作成しようとしたとき、同名の GitHub Release が既に存在しているとエラーになってしまいます。

対策として、ジョブの構成を変更し、64bit 版のジョブと 32bit 版のジョブが終わったら、リリースを行うジョブを起動するようにしました。


```yaml
jobs:
  build:
    runs-on: windows-latest

    strategy:
      matrix:
        arch: [x64, x86]

    ...

  release:
    runs-on: windows-latest
    needs: [build]
```

このように `needs` を指定すると、matrix.arch == 'x64' と matrix.arch == 'x86' の両方の build ジョブが終わってから、release ジョブが実行されます。

ジョブ間では作業ディレクトリの内容は保持されないため、データを受け渡すには artifact を使用します。build ジョブでは [actions/upload-artifact](https://github.com/actions/upload-artifact) を使って artifact をアップロードし、release ジョブでは [actions/download-artifact](https://github.com/actions/download-artifact) を使って artifact をダウンロードします。

今回、ジョブ構成を変更したことで、64bit 版と 32bit 版の両方が正しくビルドされた場合に限り、GitHub Release が作成されるようになりました。vim-win32-installer では、何らかの理由で 64bit 版はビルドされたが 32bit 版はビルドに失敗した場合、GitHub Release には 64bit 版のみがアップロードされるということが発生していました。vim-kt ではこのような中途半端な状態は発生しなくなっています。

## actions/upload-release-asset ではファイルを 1 つずつしかアップロードできない

前述の actions/upload-release-asset を使って GitHub Release にファイルをアップロードする場合、ファイルを 1 つずつしかアップロードできないという問題があります。特に、actions/upload-release-asset には引数として、ファイルのパス、ファイルの名前、ファイルの MIME タイプの 3 つを渡す必要があり、複数のファイルをアップロードする際には非常に面倒です。

対策として、公式のアクションである actions/create-release と actions/create-release を使うのはやめて、代わりに [softprops/action-gh-release](https://github.com/softprops/action-gh-release) を使うことにしました。これを使えば、GitHub Release の作成と、ファイルのアップロードをまとめて行うことができます。さらに、複数ファイルを一度に指定することや、ワイルドカードを使ってファイルを指定することができます。アップロード後のファイル名はローカルのファイル名がそのまま使われ、MIME タイプは自動判別されます。

ただ、softprops/action-gh-release には、タグ名を指定して GitHub Release を作成するという機能がなかったため、今回はそのまま使うことはできませんでした。
そこで、[タグ名を指定して作成する機能を追加し](https://github.com/softprops/action-gh-release/pull/39)、マージしてもらうことでようやく使えるようになりました。

## 一部のテストが動かない

gvim.exe および vim.exe のそれぞれにおいて、一部のテストが正しく動かない問題が発生しました。

gvim.exe で Test\_undofile\_earlier テストが失敗する問題は、結局原因が分からなかったため、[パッチを当てて](https://osdn.net/users/k_takata/pf/vim-ktakata-mq/scm/blobs/5abe48e25ccac378b79942f4eb89cee1928908d9/skip-undofile-earlier-on-gh-actions.patch)、テストをスキップすることにしました。(のちにタイミングの問題と判明し、[8.2.1273](https://github.com/vim/vim/commit/c69950ac17225d07f973b39d5b0eb94291077808) で本体のテストが修正されました。)

vim.exe で、外部コマンドを実行するテストの一部がハングアップする問題は、start コマンドを使って vim.exe を別ウィンドウで実行すれば回避できることが判明しました。(GitHub Actions がコマンドの実行結果を取得する仕組みの影響と推測。)

これらの調査に当たっては、"[Reverse RDP into Windows on GitHub Actions](https://github.com/nelsonjchen/reverse-rdp-windows-github-actions)" に記載の方法を使って、GitHub Actions にリモートデスクトップ接続を行ったりもしました。

## bash が使える

vim-win32-installer では、処理の大半をバッチファイルに記述していたため、はじめは今回もそれを元にシェルに cmd を指定していました。ただ、文字列処理などにおいてバッチファイル特有のおかしな動作に悩まされたため、シェルに bash を指定したところ、素直に記述できた部分が複数ありました。
(PowerShell は使い慣れていないので、今回は使いませんでした。)

**追記**
[msys2/setup-msys2](https://github.com/msys2/setup-msys2) を使い、`shell: msys2 {0}` と指定すれば、(Git for Windows ではなく) MSYS2 の bash をシェルとして使うこともできます。MSYS2 のツールをメインに使いたい場合はこちらの方が便利かもしれません。
なお、デフォルトでは PATH 環境変数は継承されず、git コマンドなどもそのままでは使えないことに注意する必要があります。(MSYS2 にも git をインストールするか、`path-type: inherit` を設定する。)

## スケジュール実行ではキャッシュが使えない

[actions/cache](https://github.com/actions/cache) v1 のドキュメントを見ても特に記載はありませんが、現時点では、スケジュール実行ではキャッシュが使えないようです。(使えるのは push と pull\_request のみ。)

**追記**
actions/cache@v2 からはスケジュール実行でもキャッシュが使えるようになりました。

## actions/checkout は v2 以降であればそのまま push も可能

"[git - Push to origin from GitHub action - Stack Overflow](https://stackoverflow.com/a/57932145/4570471)" などを見ると、[actions/checkout](https://github.com/actions/checkout) でチェックアウトしたリポジトリに対し、`git push` を行うには `git remote set-url` で設定が必要だと書かれていますが、actions/checkout の v2 以降であれば、特に設定を行わずとも `git push` ができます。

## エスケープシーケンスで文字に色付け

以下のように環境変数にエスケープシーケンスを設定しておけば、文字に色を付けることができます。

```yaml
env:
  COL_RED: "\x1b[31m"
  COL_GREEN: "\x1b[32m"
  COL_YELLOW: "\x1b[33m"
  COL_RESET: "\x1b[m"
```

以下のようにして使います。

```batch:cmd
echo %COL_RED%Red message%COL_RESET%
```

```bash:bash
echo ${COL_RED}Red message${COL_RESET}
```

(`"\x1b"` 表記を使わず、YAML ファイルに直接 Esc 文字を書くと、YAML ファイルのパースでエラーが発生する模様。)

## シェルとして cmd を使った時の挙動の違い

シェルとして cmd を使ったとき、AppVeyor では 1 行ずつ実行が行われるため、for 文や if 文を複数行に渡って書いたりすることはできません。

一方、GitHub Actions では 1 つのステップを 1 つのバッチファイルとして実行されるため、複数行に渡るコマンドを書いたり、ステップ内にサブルーチンを書いたりすることもできます。逆に、個々のコマンドの終了値はチェックされなくなるので、必要ならばコマンドの後ろに `|| exit 1` などと書いて明示的にコマンドの終了値をチェックする必要があります。

# まとめ

* AppVeyor と GitHub Actions の仕様の違いにより、一部のワークフローやジョブは構成を変更する必要がある。ジョブの構成は GitHub Actions の方が柔軟。
* ジョブ間のデータの受け渡しには artifact を使う。
* 公式のアクションが使いづらい場合は、第三者によるアクションを使うこともできる。
  (あるいは自分でアクションを修正して使うのも選択肢の一つ。)
* アクションを使う場合には、公式・非公式にかかわらず、バージョンアップによって機能が変更になっていないか確認が必要。
* GitHub Actions 固有の問題により、ソフトが正しく動かない場合もある。
  必要ならば、リモートデスクトップで接続して調査することも可能。
* Windows でもシェルとして bash を使うことができる。