Rust で Profile-Guided Optimization やってみた

TLDR

Rust で実装した Wasm インタープリタで PGO 試したら 0〜10% ぐらい速くなった

Profile-Guided Optimization (PGO) とは

PGO はコンパイラの最適化の手法のうちの1つですが,プログラムを実行した後に行うという点で少し他と異なります.

  1. まずは普段どおりの最適化オプションでプログラムをビルドする
  2. プログラムを実環境で動かし,profile data を取る
  3. 再度プログラムをビルドしなおす.ただし,ここの最適化では 2. で収集した profile data に基づいてインライン化やレジスタ割り当てなどを行う
  4. より最適化されたプログラムが出来上がる

このように,実世界でどう実行されるかをフィードバックした最適化をかけて再ビルドすることで,プログラムのビルド時で完結していた従来の最適化よりも進んだ最適化をかけられるようにするというものです.

最近だと,FacebookBOLT という大規模アプリ向けの最適化フレームワークを研究開発していたり,Googlellvm-propeller というこれも大規模アプリ向けのフレームワークを研究開発していたりで,最適化の分野では熱いトピックだと思ってます.あと Android のネイティブコードでも PGO が使えるみたいです

この PGO の仕組みは LLVM に既に実装されており,Clang などで利用可能で,Clang のユーザマニュアルもあります.上記の 2. において profile data を収集するために Linux の perf などを使う方法も考えられますが,LLVM を使っていれば,より正確なデータを効率的に収集し,それを元にした最適化をかけるところまで全部やってくれます.Rust の PGO サポートは LLVM のこの仕組みに完全に乗っかる形になっています.

Rust で PGO する

最近 Rust で PGO が既にサポートされているのを rustc のガイドで見かけて,僕の観測範囲ではまだ誰も試してなさそうだったので試してみました. 基本的には下記の短いドキュメントに非常にミニマルな main.rs 単一ファイルでのやり方が書いてあります.

Profile Guided Optimization - The rustc book

ただ,小さすぎるプログラムだと効果が出なさそうなのと,自分の開発してるプログラムでどれぐらい効果があるものなのか興味があったので,今回は wain プロジェクトを使ってやることにしました.

github.com

wain は僕が趣味で開発している WebAssembly インタープリタで,no dependency, Safe Rust で約1.5万行ほどの実装規模です.小さいですが,小規模というほどでもない感じですね. 外部依存は無いですが,複数 crate に分割して workspace で管理しています.

準備

PGO は Stable Rust でサポートされているようですが,一応最新の LLVM でやりたかったので nightly を使っています. rustup を使って LLVM 関連のツールをインストールします.

rustup component add llvm-tools-preview

llvm-tools-preview コンポーネントをインストールしてもパスは通されないので,~/.rustup/toolchains/{toolchain}-{triple}/lib/rustlib/{triple}/bin にパスを自前で通しておきます.

# nightly なら
export PATH=$HOME/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/bin:$PATH

# stable なら
export PATH=$HOME/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/bin:$PATH

まずは profile を有効にしてビルドする

profile data を収集するために,まずは profile data を自動生成してくれるオプションを有効にした実行ファイルをビルドします.

CARGO_BUILD_RUSTFLAGS="-Cprofile-generate=$(pwd)/pgo-data" cargo build --release

$CARGO_BUILD_RUSTFLAGS を使うことで,Cargo が rustc を使ってコンパイルする際に追加でコンパイルオプションを指定できます.

-Cprofile-generate=$(pwd)/pgo-data

というコンパイルオプションにより,カレントディレクトリの pgo-data というディレクトリに profile data が収集されていきます.

ここで注意なのですが,cargo rustc コマンドは使えません.今回は依存 crate を含むバイナリ全体を最適化したいので,依存 crate のコードでも profile data を収集する必要があります.そのため,依存 crate をビルドする際にも -Cprofile-generate オプションを指定するのですが,cargo rustc は依存 crate のビルド時には rustc に指定したオプションを渡しません.

上記 cargo build により,普段どおり ./target/release/ に最適化された実行ファイルが出来上がります(今回は ./target/release/wain

profile data を収集する

今回はお試しなので,単に examples ディレクトリにある Wasm ファイルを順番に実行することにします

./target/release/wain ./examples/brainfxxk.wasm
./target/release/wain ./examples/mandelbrot.wasm
./target/release/wain ./examples/mt19937.wasm
./target/release/wain ./examples/n_queens.wasm
./target/release/wain ./examples/pi.wasm
...

これによって,./pgo-data 内に .profraw という拡張子のバイナリファイルが生成されます..profraw ファイルは実行ファイルと動的リンクライブラリの分だけ生成されるので,例えば2つライブラリを動的リンクしている実行バイナリであれば,3つの .profraw ファイルが生成されます.

wain は動的リンクライブラリを使っていないので,1つだけ .profraw が生成されます.

> ls ./pgo-data
default_13294579539908499303_0.profraw

./target/release/wain に異なる引数を与えて新しい Wasm コードを実行するたびに,収集した profile data が自動でこのファイルに追加されていきます.各 Wasm ファイルを ./target/release/wain で実行しながら default_13294579539908499303_0.profraw を見ると,徐々にファイルサイズが大きくなっていくのが分かります.

profile data を1つのファイルにマージする

前節で説明したように,profile data を収集した .profraw ファイルは複数になることがあります.また,今回は手元の PC でしか profile data を収集していませんが,例えば複数の異なる本番環境で実行した profile data を1箇所に持ってきたりすると必然的に profile data は複数ファイルになります.

これらを llvm-profdata コマンドで1つのファイルにマージします.llvm-profdata コマンドは準備の節で説明した llvm-tools-preview コンポーネントに含まれています.

llvm-profdata merge -o merged.profdata ./pgo-data

このコマンドを走らせるだけで,マージした profile data ファイル merged.profdata を生成してくれます.

profile data を元にした最適化を有効にしてビルド

収集した merged.profdataコンパイラに食わせて,PGO を行います.

CARGO_BUILD_RUSTFLAGS="-Cprofile-use=$(pwd)/merged.profdata" cargo build --release

このように -Cprofile-use オプションで指定してやるだけです.後は rustc と LLVM がすべてやってくれます.-Cprofile-generate の場合と同様に,cargo rustc は使えないので注意してください.依存 crate を含むプログラム全体で PGO を有効にします.

これで ./target/release/wain に PGO を有効にしてビルドされた実行ファイルが生成されます.通常のリリースビルドでサイズが 716KB,PGO を有効にすると 783KB でわずかにサイズが増えていました.

どれぐらい速くなったのか測る

PGO を有効にした wain と通常のリリースモードでビルドした wain を用意します.

mv ./target/release/wain wain-pgo
cargo build --release
mv ./target/release/wain wain-orig

profile data の収集に使った examples ディレクトリにある Wasm ファイルhyperfine を使って比較します.hyperfine は比較したいコマンドを与えると良い感じに実行・計測して比較してくれるすごいやつです.

hyperfine './wain-pgo ./examples/brainfxxk.wasm' './wain-orig ./examples/brainfxxk.wasm'
hyperfine './wain-pgo ./examples/mandelbrot.wasm' './wain-orig ./examples/mandelbrot.wasm'
hyperfine './wain-pgo ./examples/mt19937.wasm' './wain-orig ./examples/mt19937.wasm'
hyperfine './wain-pgo ./examples/n_queens.wasm' './wain-orig ./examples/n_queens.wasm'
hyperfine './wain-pgo ./examples/pi.wasm' './wain-orig ./examples/pi.wasm'
...

計測結果

examples/brainfxxk.wasm

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/brainfxxk.wasm 25.0 ± 1.9 23.6 40.1 1.06 ± 0.10
./wain-pgo examples/brainfxxk.wasm 23.5 ± 1.2 22.0 28.3 1.00

examples/mandelbrot.wasm

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/mandelbrot.wasm 234.1 ± 5.5 227.3 248.3 1.09 ± 0.03
./wain-pgo examples/mandelbrot.wasm 214.6 ± 3.0 211.1 220.4 1.00

examples/mt19937.wasm

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/mt19937.wasm 74.4 ± 1.7 72.6 79.9 1.07 ± 0.03
./wain-pgo examples/mt19937.wasm 69.7 ± 1.6 67.8 75.2 1.00

examples/n_queens.wasm

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/n_queens.wasm 9.2 ± 0.6 8.5 12.9 1.06 ± 0.11
./wain-pgo examples/n_queens.wasm 8.7 ± 0.6 8.0 12.1 1.00

examples/nbodies.wasm

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/nbodies.wasm 131.1 ± 2.7 128.2 139.1 1.12 ± 0.03
./wain-pgo examples/nbodies.wasm 117.3 ± 2.3 114.9 125.0 1.00

examples/pi.wasm

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/pi.wasm 115.7 ± 2.8 113.2 124.6 1.04 ± 0.04
./wain-pgo examples/pi.wasm 111.6 ± 2.7 109.2 120.3 1.00

examples/primes.wasm

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/primes.wasm 2.6 ± 0.4 2.2 5.1 1.01 ± 0.21
./wain-pgo examples/primes.wasm 2.6 ± 0.4 2.3 6.0 1.00

examples/quicksort.wasm

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/quicksort.wasm 2.5 ± 0.4 2.0 5.4 1.00
./wain-pgo examples/quicksort.wasm 2.5 ± 0.5 2.0 6.3 1.02 ± 0.25

examples/sqrt.wasm

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/sqrt.wasm 2.6 ± 0.4 2.1 5.2 1.00 ± 0.20
./wain-pgo examples/sqrt.wasm 2.6 ± 0.4 2.1 5.2 1.00

examples/brainfxxk.wat

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/brainfxxk.wat 25.3 ± 1.0 24.0 29.2 1.08 ± 0.07
./wain-pgo examples/brainfxxk.wat 23.4 ± 1.1 22.3 30.9 1.00

examples/mandelbrot.wat

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/mandelbrot.wat 234.5 ± 5.0 229.0 244.1 1.08 ± 0.03
./wain-pgo examples/mandelbrot.wat 217.2 ± 3.5 212.6 223.2 1.00

examples/mt19937.wat

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/mt19937.wat 75.2 ± 1.5 73.6 81.1 1.06 ± 0.04
./wain-pgo examples/mt19937.wat 70.6 ± 2.2 68.4 77.9 1.00

examples/n_queens.wat

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/n_queens.wat 9.8 ± 0.7 9.0 12.3 1.05 ± 0.10
./wain-pgo examples/n_queens.wat 9.3 ± 0.6 8.6 11.9 1.00

examples/nbodies.wat

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/nbodies.wat 132.2 ± 3.2 128.0 139.8 1.12 ± 0.03
./wain-pgo examples/nbodies.wat 117.9 ± 2.0 115.7 122.6 1.00

examples/pi.wat

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/pi.wat 115.8 ± 2.8 112.6 122.0 1.04 ± 0.03
./wain-pgo examples/pi.wat 111.3 ± 2.3 109.2 118.4 1.00

examples/primes.wat

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/primes.wat 2.9 ± 0.4 2.5 6.1 1.01 ± 0.22
./wain-pgo examples/primes.wat 2.9 ± 0.4 2.4 5.6 1.00

examples/quicksort.wat

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/quicksort.wat 2.8 ± 0.4 2.4 6.2 1.00
./wain-pgo examples/quicksort.wat 2.8 ± 0.4 2.3 6.6 1.00 ± 0.19

examples/sqrt.wat

Command Mean [ms] Min [ms] Max [ms] Relative
./wain-orig examples/sqrt.wat 2.9 ± 0.4 2.4 5.3 1.01 ± 0.19
./wain-pgo examples/sqrt.wat 2.8 ± 0.4 2.4 5.3 1.00

ほぼ変わらないものもありますが,0〜10% ほど速くなっていることが分かります.実行対象がほぼ決まっているプログラムであれば,プログラムに直接手を入れること無く最大 10% の高速化が得られると考えるとかなりお得に見えます.

ただし,もちろん profile data 収集時に使わなかった入力では逆に遅くなる可能性もあるので,実運用時には profile data をどう上手く収集するかがカギの1つになりそうです.

ちなみに毎回手でコマンドを打つのが面倒だったので数十行のシェルスクリプトを書いてやってました.

https://gist.github.com/rhysd/1324f6726f8905f4e71c0f549cb35d97

実際に実行したコマンドを知りたい場合は参照してみてください

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 のデプロイができました.