GitHub Action で Vim や Neovim を簡単にインストールできる action-setup-vim をつくった

今週ちまちまと git-messenger.vimclever-f.vim の CI を GitHub Actions に移行していました.毎回 Vim プラグインの CI のために Vim や Neovim のセットアップを書くのが面倒なのと,Windows 上で Vim や Neovim を入れるのが(Powershell に不慣れなこともあり)大変だったので,GitHub Action として切り出すことにしました.

github.com

1ステップで Vim や Neovim を簡単にインストールできます.

  • Vim と Neovim 両対応
  • Linux, macOS, Windows すべてで動作
  • 'stable' と 'nightly' の両方に対応
  • 追記: 特定バージョンにも対応(v1.1.0)

使い方

下記のようにステップを書けば Vim または Neovim をインストールしてくれます.

macOS または Linux で安定版 Vim をインストール:

- uses: rhysd/action-setup-vim@v1

Windows では github-token を input として与える必要があります.これは,vim-win32-installer から最新のリリースを持ってくる必要があり,そのために GitHub API を叩いているからです.

- uses: rhysd/action-setup-vim@v1
  with:
    github-token: ${{ secrets.GITHUB_TOKEN }}

最新の Vim をインストールするには version: nightly を input に指定します.

- uses: rhysd/action-setup-vim@v1
  with:
    version: nightly
    github-token: ${{ secrets.GITHUB_TOKEN }}

Neovim をインストールするには neovim: true を input に指定します.最新の stable の Neovim がすべての OS でインストールできます.Vim とは異なり,Windows 上でも github-token input は不要です.

- uses: rhysd/action-setup-vim@v1
  with:
    neovim: true

昨晩ビルドされたばかりの nightly の Neovim をインストールするには version: nightly を指定すれば OK です.

- uses: rhysd/action-setup-vim@v1
  with:
    neovim: true
    version: nightly

これらのステップを実行後,Vim をインストールした場合は vim コマンドが,Neovim をインストールした場合は nvim コマンドがそれぞれ利用可能になっているはずです.

また,action の executable output としてインストールした Vim または Neovim の実行ファイルへのフルパスをセットしていて,それを使うこともできます.

例えば checkout@v2themis.vim をインストールし,action-setup-vimVim をインストールして単体テストを走らせる例は

# テストしたいプラグインを checkout
- uses: actions/checkout@v2
# themis.vim を checkout
- uses: actions/checkout@v2
  with:
    repository: thinca/vim-themis
    path: vim-themis
# Vim をインストール
- uses: rhysd/action-setup-vim@v1
  id: vim
# プラグインの単体テストを themis.vim で実行
- name: Run unit tests with themis.vim
  env:
    THEMIS_VIM: ${{ steps.vim.outputs.executable }}
  run: |
    ./vim-themis/bin/themis ./test

実際に clever-f.vim のワークフロー で利用しています.

インストールされる Vim および Neovim の詳細

インストール元は OS と version input によって下記のようになっています.優先度は

  1. システムのパッケージマネージャ
  2. 公式リリース
  3. ソースからビルド

となっています.ユーザ数や実行の速さを考慮してこうなっています.

Vim

OS Version Installation
Linux stable gvim-gnome パッケージを apt でインストール
Linux nightly vim/vim リポジトリの HEAD をビルド
macOS stable brew install macvim で Homebrew からインストール
macOS nightly vim リポジトリの HEAD をビルド
Widnows stable Windows での公式安定版は無いので,Nightly と同じ
Windows nightly 公式インストーラ repository のリリースからインストール

Neovim

OS Version Installation
Linux stable Neovim 公式の stable release からインストール
Linux nightly Neovim 公式の nightly release からインストール
macOS stable Homebrew を使って brew install neovim でインストール
macOS nightly Neovim 公式の nightly release からインストール
Windows stable Neovim 公式の stable release からインストール
Windows nightly Neovim 公式の nightly release からインストール

制限

今のところ,特定のバージョンを指定してインストールはできません.技術的制約があるわけではないので,もし需要があればやるかもしれません. v1.1.0 で指定可能になりました.

# Vim v8.2.0126 を全ての OS でインストール.Windows でも github-token input は必要ありません
- uses: rhysd/action-setup-vim@v1
  with:
    version: v8.2.0126

# Neovim v0.4.3 を全ての OS でインストール
- uses: rhysd/action-setup-vim@v1
  with:
    neovim: true
    version: v0.4.3

また,GUI バージョンについては現状ではサポートしていませんが,リリース物に含まれる場合はインストールされます.具体的には下記の場合です:

まとめ

Vim または Neovim を簡単にインストールできる action action-setup-vim を作成しました. GitHub Action の Vim プラグイン CI への導入の敷居が個人的にぐっと下がったので,他のプラグインについても順次移行していきたいところ.

GitHub Actions の JavaScript Action を TypeScript で書いた

GitHub Action を TypeScript で作成したので,覚え書きがてらどうやって作ったかについて書きます. github-action-benchmark という Action をつくりました.

紹介記事:継続的にベンチマークを取るための GitHub Action をつくった

Action とは

今年9月に GitHub Action v2 がリリースされました.GitHub Action は GitHub が提供する CI/CD サービスです. 既存のサービスと大きく違う点は,処理を汎用的に Action として切り出して再利用できることです. 例えば,GitHub からのリポジトリのクローン actions/fetch や Node.js のセットアップ actions/setup-node などの基本的な実行ステップも Action として実装されています.

Action の種類

GitHub Action には JavaScript Action と Docker Action があります.下記のページに公式の解説があります.

https://help.github.com/en/actions/automating-your-workflow-with-github-actions/about-actions

この記事では後者のみに絞ります.

Hello world

まず JavaScript で雛形を書きます. Creating a JavaScript action を見て順番に試していけば良いので,ここで解説することは特にありません. action.yml を置き,npm init して @actions/* パッケージを入れ,index.js を書きます.

次に npm install --save-dev typescript して TypeScript コンパイラを入れ,index.jsindex.ts に書き換え,tsconfig.json を用意します.

{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node",
    "preserveConstEnums": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noEmitOnError": true,
    "strictNullChecks": true,
    "target": "es2019",
    "sourceMap": true,
    "esModuleInterop": true,
    "resolveJsonModule": true
  },
  "files": [
    "index.ts"
  ]
}

もしくは公式のテンプレートを使っても良いです.

公式の @actions/* パッケージは TypeScript で書かれていて型定義を持っているのでこのままでほぼ問題ありませんが,残念ながら一発ではコンパイルが通りません.

@actions/github が使っている @octokit/graphql は v4 から TypeScript に対応しているのですが,@actions/github が使っているのは v2 なので型定義がありません. 少し調べてみると,@actions/github@octokit/graphql の型定義を自前で抱えていることが分かったので,v4 に対応するまではそこのものをコピーしてくるか,下記のように回避するための型定義ファイルを書いて自分のプロジェクトに含めておく必要がありました.

// Workaround for @actions/github until @octokit/graphql v4 is used
declare module '@octokit/graphql' {
    type GraphQlQueryResponse = unknown;
    type Variables = unknown;
}

最後にテスト用の workflow ファイルを更新します.TypeScript のコンパイルが必要になるので,リポジトリを checkout してきて node をセットアップし,npm install して tscコンパイルします. 実行する Action を ./ で始まる相対パスにしておくとローカルのアクションを実行してくれます. ちなみに . ではダメで ./ にする必要があって,少しハマりました.

      runs-on: ubuntu-latest
      name: A job to say hello
      steps:
+     - uses: actions/checkout@v1
+     - uses: actions/setup-node@v1
+     - run: npm install
+     - run: ./node_modules/.bin/tsc -p .
+     - name: Hello world action step
        id: hello
-       uses: actions/hello-world-javascript-action@master
+       uses: ./
        with:
          who-to-greet: 'Mona the Octocat'
      # Use the output from the `hello` step

後は自分の好きなように入力を定義し,処理を書いて結果を出力するだけなので,基本的に下記のドキュメントを見て進めれば OK です.

この記事では今回 github-action-benchmark を作成する際に引っかかったポイントや考えたポイントだけ書きます.

Action の入力

Action への入力は workflow ファイルでは with:key: value で複数していします.JavaScript Action では @actions/core パッケージの getInput(name: string): string で値を取得できます.

少し注意する必要があるのは,入力は文字列型オンリーな点です.この入力には array や map が使えません.なので,例えば下記のようなリスト形式やキーバリュー形式の入力を定義することはできません.

uses: ./
with:
  awesome-array-option:
  - one
  - two
  - three
  awesome-map-option:
    key: value
    key2: value2

これは入力が Docker Action で環境変数 $INPUT_*マッピングされていることに由来していそうですが,理由は不明です.

リストを使いたい場合はどうすれば良いのかなといくつか公式 Action を調べてみると,

with:
  awesome-array-option: |
    one
    two
    three

のように改行区切りの文字列で指定させて Action 側で処理しているものがありました.

Action のデバッグ

GitHub Action の実行環境と同じ環境変数(例えば入力 foo-bar$INPUT_FOO_BAR)を与えて node index.js で実行するか,Action をプッシュしてテスト用の workflow の実行結果を見てデバッグすることになります.簡単なものであれば前者だけで事足りますが,最終的に GitHub Action で実行して結果を確認することになると思います.

@actions/corecore.debug()デバッグ情報を埋め込んでおくのですが,これは(デバッグ情報なので当然ですが)デフォルトでは表示されません.

デバッグ情報は特定の secret に true をセットすることで初めて表示されます.secret は本来リポジトリ固有の秘密の情報(API トークンとか)を保存する場所ですが,デバッグ情報の設定にも利用されています.リポジトリページ → Settings → Secrets タブ で設定できます.

  • ACTIONS_RUNNER_DEBUG: workflow を実行する Runner の実行ログ
  • ACTIONS_STEP_DEBUG: 各ステップの実行ログ

Action のデバッグ情報を見るには後者が必要です.

Action のテスト

単体テスト

普通の npm パッケージと同じようにテストすれば OK です. GitHub Action と同じ環境変数を与えてテストを実行すれば OK です.

github-action-benchmark では単体テスト中に git コマンドを実行されると困るのと,@actions/core の実装のロジックに依存するのが嫌だったので @actions/coremock-require で mock してしまってます. 真面目にやるならテスト用の Git リポジトリを作ってその中でテストケースを走らせるべきですが,その辺はサボってます.

E2Eテスト

実際に Action を走らせて期待した動作になっているかは GitHub Action で走らせて結果を見るのが一番良いと思います. github-action-benchmark では CI 用の workflow の中で action をビルドして実行し,実行後に期待した状態になっているかをスクリプトでチェックしています.

これだと「正しく fail するか」をテストすることができない(action が fail するとその時点で workflow が止まってしまうので)のですが,どうやれば良いかはまだよく分かっていないです…(誰かご存知なら教えてもらえると嬉しいです)

Action の公開

Action は GitHub の公開リポジトリに置いておくと即使えるようになります.例えば user/foo-action というリポジトリをつくると

name: user/foo-action@ref

のように書くだけで使えるようになります.ここで ref は Git リポジトリの ref で,ブランチ名やタグ名,コミットハッシュなどが使えます. なので,この ref で Action のバージョンを制御するのですが,セマンティックバージョニングをサポートするような仕組みは無いので,自前でルールを定める必要があります.

github-action-benchmark ではセマンティックバージョニングに従ってリリースすることを決めました.

  • rhysd/github-action-benchmark@v1 は v1.x.y の最新を表す
  • rhysd/github-action-benchmark@v1.1.0 は v1.0.0 に固定することを表す
  • マイナーバージョンまでの固定や範囲指定はサポートしない(必要性を感じなかったので)

としました.

また,JavaScript Action は Action のリポジトリを checkout した時点で即 node . で実行できるようになっていないといけないので,TypeScript や babel を使っている場合は事前にコンパイルした JavaScript コードをリポジトリに置いておく必要があります. また,Action 実行時に npm install も行わないので node_modules も丸ごと置いておく必要があります.native extension を利用しているパッケージが node_modules に含まれている時にどうすれば良いのかはよく分かりません…

master ブランチにビルドした生成物と依存パッケージを直置きしたくなかったので,下記のように運用することにしました

  • 各メジャーバージョンごとに orphan ブランチを切る.例えば v1.x.y 向けは v1 ブランチ
  • 新しいバージョンのリリースは対応するメジャーバージョンのブランチで行う.例えば現在の master ブランチを v1.0.2 にリリースする場合は,まずは master ブランチで TypeScript コードを JavaScriptコンパイルし,v1 ブランチに切り替えてそれらを add して commit し,v1.0.2 タグをつけて push します
  • 次のメジャーバージョン(例えば v2)をリリースする時は前バージョンの開発用ブランチ(例えば dev/v1)を作っておいて v1.x.y 向けの修正はこちらから v1 ブランチに入れる

f:id:rhysd:20191115211932p:plain
branch 戦略

ちなみに公式の推奨では release/v1 ブランチをつくって v1 タグを毎回最新に張り替えることを推奨しています.これはリリース前に release/v1 上でテストを行う前提なんだと思いますが,毎回張り替えないといけないのと,master でテストしてから v1 ブランチに成果物を置く前提だったのでブランチ名を v1 としています.

ちなみに masterv1 へは簡単なスクリプトを使って成果物を置いてます.

https://github.com/rhysd/github-action-benchmark/blob/master/scripts/prepare-release.sh

Action で発行される GitHub API トークンの注意

GitHub Action では実行ごとに GitHub APIトークンが自動生成され secrets.GITHUB_TOKEN に格納されます.基本的には普通の API トークンのように使えるのですが,どうやらパーソナルアクセストークンとは異なりイベントを起こす権限を持っていないようです. 例えば GitHub Pages のブランチに push することはできますが,GitHub Pages のデプロイをすることはできません.

https://github.community/t5/GitHub-Actions/Github-action-not-triggering-gh-pages-upon-push/td-p/26869

この制限のため,github-action-benchmark ではパーソナルアクセストークンが必要になってしまっています.

ちなみにこの制限はなぜか public repo のみで,private repo では問題なく GitHub Pages のデプロイができました.

継続的にベンチマークを取るための GitHub Action をつくった

今年9月に GitHub Action v2 がリリースされました.GitHub Action は GitHub が提供する CI/CD サービスです. 既存のサービスと大きく違う点は,処理を汎用的に Action として切り出して再利用できることです. 例えば,GitHub からのリポジトリのクローン actions/fetch や Node.js のセットアップ actions/setup-node などの基本的な実行ステップも Action として実装されています.

今回はこの GitHub Action を利用して,前々からあると良いなと思っていたベンチマークを継続的に取るための Action をつくりました.

github.com

github-action-benchmark はベンチマークの実行の出力からベンチマーク結果を抽出し,GitHub pages のブランチに JSON で保存します. 保存された JSONGitHub pages でグラフチャートで確認できます.

f:id:rhysd:20191111131334p:plain
スクリーンショット

実際にベンチマークを取って表示しているデモは下記の URL で確認できます:

https://rhysd.github.io/github-action-benchmark/dev/bench/

ベンチマークツールの出力からベンチマーク結果を抽出するところでツール固有のロジックが必要で,現在は下記のツールに対応しています.

解決したい問題

アプリやライブラリの実行パフォーマンスを見るための手段としてベンチマークがあります. ベンチマークの結果は最適化の際に効果を検証するのに使ったり,開発の中で意図せずパフォーマンスがデグレしていないことを確認したりするために使うのが一般的だと思います. ここでは後者にフォーカスを当てます.

それなりにメジャーな言語には大抵ベンチマークを取るための公式ライブラリやデファクトなライブラリがあります. それらのツールはベンチマークを実行して結果を表示したり,指定したリビジョン間での比較を行ったりはできますが,開発の中で継続的に計測結果をモニタするような仕組みは提供してくれません.

ですが,意図しないパフォーマンスのデグレを防ぐには継続的にベンチマーク結果をチェックする必要があります. それを GitHub 上のリポジトリに絞って行うための Action が github-action-benchmark です.

使い方

Examples

リポジトリ内に Rust, Go, JavaScript のそれぞれの実際に動くデモを置いてあります.これらのデモは実際に github-action-benchmark のリポジトリで CI の一部として実行されています.

これらのワークフローを実行して収集されたログが github-action-benchmark の GitHub pages のページにホストされています.

github-action-benchmark は複数のベンチマーク結果を1箇所に集められるため,複数の言語で開発されているリポジトリや monorepo な構成のリポジトリでも問題なく使えます.

余談ですが,全てのプロジェクトでフィボナッチ数を求めるベンチマークを回していて,各言語のフィボナッチ数計算に対する実行パフォーマンスが何となく分かりますね.この超簡単な例では Rust が一番速く,Go は Rust に比べて1.6倍ほど遅く,Node.js は Go に比べて2倍ほど遅いです.

ワークフローの書き方

上記の Examples のワークフロー設定(YAML)を真似すれば問題ないと思いますが,一応ワークフローの書き方を説明します.どの言語でもほぼ変わらないので,ここでは Go プロジェクトを仮定します.

まずはベンチマークの出力を取得するステップを追加します.github-action-benchmark はベンチマーク出力を含むテキストファイルを入力とするので,tee コマンドなどを使ってベンチマーク結果をログに出しつつ保存します.

- name: Run benchmark
  run: go test -bench 'Benchmark' | tee output.txt

ここで得た output.txt を使って github-action-benchmark を実行します.

- name: Store benchmark result to gh-pages
  uses: rhysd/github-action-benchmark@v1
  with:
    name: My Project Go Benchmark
    tool: 'go'
    output-file-path: output.txt

ここで,uses: rhysd/github-action-benchmark@v1リポジトリ https://github.com/rhysd/github-action-benchmarkv1 をチェックアウトして使いますという宣言です.github-action-benchmark では v1 ブランチに v1.x.y の最新のバージョンをデプロイしているので,それがチェックアウトされます.もしバージョンを固定したければ v1.0.2 のようなタグも提供しています.

with: のセクションはこのアクションへの入力です.

  • nameベンチマークの名前です.複数のベンチマークを取る場合はこの値がリポジトリ内で一意になるようにしてください. 指定しない場合のデフォルト値は "Benhcmark" です.
  • tool はどのツールを使ってベンチマーク出力を取得したかを示すものです."cargo", "go", "benchmarkjs" の中のいずれかの値を指定する必要があります.
  • output-file-pathベンチマークの出力が保存されているファイルへのパスを指定します.リポジトリからの相対パスで記述できます.

これ以外にも,入力や出力をカスタマイズするためにいくつかの入力が定義されています.詳しくは下記の README の表を参照してください.

https://github.com/rhysd/github-action-benchmark#action-inputs

github-action-benchmark を実行すると,gh-pages ブランチの dev/bench/ 以下に結果が JavaScript ファイル data.js として出力され,(もし既に無ければ)同じディレクトリにグラフチャートを見るための index.html が生成され,それらを含むコミットが作られます. ブランチや生成場所のディレクトリパスは上記の with: に書く入力で変えられます.

最後にブランチをリモートに push します.github-action-benchmark がコミットの生成まで行い push をしないのは,Action は(GitHub API トークンを入力で受け取るなどしない限り)リモートに push する権限を持っていないからです. ここでは安全側に倒して github-action-benchmark 内では push しない設計にしました.

追記(2019/11/11 20:28): どうやら Action は現在 private リポジトリのみで GitHub pages をデプロイすることが可能なようです.いただいた情報によるとこれは GitHub Action 側の問題らしいので,いずれ解消されて GitHub pages への push は github-action-benchmark 側でできるようになるかもしれません(thanks to @pris314 さん

GitHub Action ではワークフローの実行で GitHub API トークンを自動で発行してくれるので,push するのは簡単です.

- name: Push benchmark result
  run: git push 'https://you:${{ secrets.GITHUB_TOKEN }}@github.com/you/repo-name.git' gh-pages:gh-pages

トークンは secrets に保存されているのでそれを展開して git push します.もちろん実行ログでは secrets の展開部分は隠されます.

ちなみに複数のベンチマークを複数ワークフローで並列で実行している場合は,他のワークフローによって GitHub pages のブランチが更新されていて直接 push できない場合があるので,push 前に git pull --rebase などを挟んでください.

また,github-push-action を使う手もあります(push は自前でやるのがおすすめですが).

- name: Push changes
  uses: ad-m/github-push-action@master
  with:
    branch: gh-pages
    github_token: ${{ secrets.GITHUB_TOKEN }}

gh-pages ブランチがリモートにまだ無い場合は作成しておいてください.

$ git checkout -b --orphan gh-pages
$ git commit -m 'first commit' --allow-empty
$ git push origin gh-pages

これでワークフローが走ったときにベンチマーク結果が gh-pages 内に保存され,https://you.github.io/repo-name/dev/bench で確認できるようになります.

ページを開くとベンチマークのテストケースごとにグラフが表示されます.縦軸がベンチマークの値,横軸がコミットで,右に行くほど新しいコミットを示しています.これを確認することで,ベンチマーク結果の変化が確認できます.

グラフのデータポイント上にマウスカーソルを置くと

  • コミットハッシュ
  • コミットメッセージ
  • コミットの作成日時とコミッター
  • ベンチマークの値

が表示されます.

tooltip

さらにここでクリックすると GitHub 上でそのコミットのページが開くので,そこでコードの変更内容を確認できます.

また,最下部にベンチマークデータをダウンロードできるボタンを置いてあるので,それをクリックするとベンチマーク結果を JSON としてダウンロードできます.

download button

もし GitHub pages のブランチにデフォルトで生成される index.html が気に入らなければ,好きに変更したり自前で作ったものに差し替えて OK です.github-action-benchmark は既に index.html がある場合は関与しませんし,データは全て data.js 側にあって window.BENCHMARK_DATA に格納されています.

使用の注意点

上記で説明したとおり,github-action-benchmark はリポジトリ上のブランチを変更し,それをワークフロー側でリモートに push するユースケースを想定しています.

なので,pull request ではこの Action が実行されないように注意してください.万一悪意あるユーザが pull request を作成し,gh-pages に悪意ある変更を加える処理を入れて pull request を投げると,それが反映されてしまいます.

ワークフローの指定で master のみで実行するように制限するか,

on:
  push:
    branches:
      - master

ジョブの実行ステップに記述できる if: セクションで pull request で gh-pages ブランチを push するステップを実行しないようにガードしてください.if: の書き方は公式ドキュメントを読むと分かります:

https://help.github.com/en/actions/automating-your-workflow-with-github-actions/contexts-and-expression-syntax-for-github-actions

GitHub Action 実行環境の注意

そもそも GitHub Action の実行環境がベンチマークに適しているのか?という疑問が残っていると思います.

ずっと同じ内容の単純なベンチマークを実行しているデモページでは大体 10~20% のブレがどのベンチマークでも出ています. そのブレを許容できない場合は使えませんし,許容できるなら使えます.そこはプロジェクト次第だと思います.また,あまり無いと思いますが,ネットワークなどのリソースが絡むともっとブレ幅が大きくなる可能性があります.

安定した環境を自前で用意し,self-hosted runner として使うのも手だと思います.

今後

  • 以前のベンチマーク結果と比較して結果が極端に悪くなっている(e.g. 2倍以上遅くなっているなど)時にコミットへのコメントで通知する機能
  • ベンチマークツールの出力フォーマットに JSON を追加し,ユーザがベンチマーク結果を JSON で用意することで,どんなベンチマークツールでも対応できるようにする
  • ベンチマーク結果を GitHub pages にデプロイする代わりに Elasticsearch や Mackerel のようなサービスにアップロードする機能
  • GitHub pages のページとして出力する以外に生の JSON データを生成するオプションを追加する.ベンチマーク結果だけ JSON で欲しくて,後の処理は自前でやりたいときに便利

などがアイデアとしてありますが,今のところ機能的に自分のユースケースでは満足しているため,あまり追加の予定はありません. もし何かアイデアがあれば,issue や pull request で提案していただくのは歓迎です.

感想

GitHub Action のための Action を初めて作成しました.kiro-editor のためにつくったので,早速活用していきたいと思います.

ちなみに JavaScript Action として,TypeScript で作ってみました.GitHub Action 自体まだリリースされたところで情報がほとんどありませんが,実行は速くバグにも逢わなかったので,手探りでもあまり困ったことはありませんでした.微妙にハマったポイントもありましたが,それは別エントリで書こうかなと思います.