GitHub Actions (beta) が使えるようになったので調査した
GitHub Actions Open Beta に申し込んで1ヶ月以上経ち,ようやく使えるようになったみたいなので実際にどう使うのか調査してみたメモ. Beta 版は GitHub のプライベートリポジトリにしか使えないため,公開リポジトリに使うにはもう少し待つ必要がありそう.
GitHub Actions とは
GitHub Universe 2018 で発表されたときにメディアが記事にしているので,そちらを読んでください.
- https://www.publickey1.jp/blog/18/github_actionsdockergithub_universe_2018.html
- https://cloud.watch.impress.co.jp/docs/event/1148428.html
- https://codezine.jp/article/detail/11170
公式ドキュメント
hello world 的なのは下記のリンクから action をつくるチュートリアルと workflow をつくるチュートリアルをやれば良いです.
全体的な流れは,
Dockerfile
とentrypoint.sh
をつくって欲しいアクションを定義する(プリセットのアクションしか使わない場合はこのステップは不要)- GitHub の Actions タブに行き,workflow をビジュアルエディタで定義する.ワークフローの起点と,そこからのアクションの連なりをグラフエディタでつくれる.直接
.github/
以下を書いても良いけど,こっちのほうがチェックもしてくれるし良さそう - 実際に起点となるイベントを起こしてみる(例えば
git push
) - 再び Actions タブに行くと,各ワークフローがどう走って結果がどうだったか(ログなど)が確認できる
- ワークフローを編集したい時は
.github/
以下の.workflow
ファイルの Edit ボタンをクリックすると再びビジュアルエディタが立ち上がる
参考リンク:
- GitHub Actions
- About GitHub Actions
- Creating a workflow with GitHub Actions
- Creating workflows
- Workflow configuration options
基本
各アクションはアクションを走らせる Docker コンテナのための Dockerfile
と処理のエントリポイントになるシェルスクリプト entrypoint.sh
の組み合わせで定義できます.
Dockerfile
のラベルでメタ情報(description とか action name とか)を記述します.実際の処理は entrypoint.sh
(もしくは entrypoint.sh
から呼ばれるスクリプトなど)でシェルスクリプトで定義します.依存しているツール(例えば JSON を扱うなら jq
とか)は Dockerfile
をビルドするときにコンテナに apt install
などでインストールしておきます.
コンテナ実行時の情報はスクリプトの引数か,環境変数で与えられます.環境変数はアクションをワークフローエディタで編集する時に指定でき,スクリプト側からそれらが参照できます.またスクリプトの引数もアクションの編集で指定でき,entrypoint.sh
の引数としてアクション側に渡ってきます.秘密の情報 (secrets) はリポジトリページの settings から secrets タブを選択してキー・バリューで秘密の情報を入力しておき,アクションの設定でどのキーを使うかを指定しておくと,Docker コンテナから環境変数としてそれらが見える?ようです(env
コマンドの出力はフィルタされているのか確認できなかった)
アクションの起点は GitHub の公式ドキュメントに乗っている一覧のイベント から選べます.例えば on: "push"
と指定するとコミットをプッシュするたびにワークフローが走ります.
どうやってアクションを書くのか
注:特に明記しない限り,実動作ベースでの調査をしたので,今後動作が変わったり,勘違いしている箇所があるかもしれません
公式のアクション
actions organization に各アクションごとにリポジトリが置かれているので,それを参考にできます.
アクションが実行される環境
pwd
で確認すると,アクションのエントリポイントとなる entrypoint.sh
は /github/workspace
というディレクトリ内で実行されています.
このディレクトリについては下記の公式ドキュメントに詳細がありました:
どうやら対象のリポジトリのルートディレクトリになっているらしいです.試しに ls -la
してみると
total 24 drwxr-xr-x 6 root root 4096 Dec 9 13:54 . drwxr-xr-x 5 root root 4096 Dec 9 13:55 .. drwxr-xr-x 8 root root 4096 Dec 9 13:54 .git drwxr-xr-x 2 root root 4096 Dec 9 13:54 .github drwxr-xr-x 2 root root 4096 Dec 9 13:54 action-a drwxr-xr-x 2 root root 4096 Dec 9 13:54 action-b
.git
ディレクトリが置かれており,リポジトリのルートにいると分かります.
すでにリポジトリはクローンされた状態で実行されるので,自前で対象のリポジトリをクローンしてくる必要は無さそうです.
アクション内で参照できる環境変数
どうやら $GITHUB_*
という環境変数に情報が入っているようです.一覧は公式ドキュメントにあり,
例えば,on: "push"
でリポジトリへの push を行った際の環境変数は下記です:
GITHUB_EVENT_PATH=/github/workflow/event.json GITHUB_WORKFLOW=hello GITHUB_ACTION=Hello World GITHUB_REPOSITORY=rhysd/hello-github-actions GITHUB_WORKSPACE=/github/workspace GITHUB_SHA=52875f0b1ed9882770c0cfddbcfe95607e4b2986 GITHUB_ACTOR=rhysd GITHUB_REF=refs/heads/master GITHUB_EVENT_NAME=push
これでどのワークフローやアクションとして自身が実行されているかを知ることができます.
アクション内で参照できるフックイベントの情報
ちなみに $GITHUB_EVENT_PATH
の /github/workflow/event.json
には起点になったイベントの情報が JSON で入っています.各イベントごとに入っている情報はGitHub の公式ドキュメントで知ることができます.詳細なイベントフックの情報が欲しい場合はこっちを見たほうが良さそうです.
例えば on: "push"
での event.json
の中身は下記です:
{ "after": "c776e7146a031950fa579791f71448336a07880c", "base_ref": null, "before": "52875f0b1ed9882770c0cfddbcfe95607e4b2986", "commits": [ { "added": [], "author": { "email": "my-email@example.com", "name": "rhysd", "username": "rhysd" }, "committer": { "email": "my-email@example.com", "name": "rhysd", "username": "rhysd" }, "distinct": true, "id": "c776e7146a031950fa579791f71448336a07880c", "message": "check event.json", "modified": [ "action-a/entrypoint.sh" ], "removed": [], "timestamp": "2018-12-09T21:39:47+09:00", "tree_id": "7d4c96c54a304cd3af1bdbac684099bbbeb1dcc9", "url": "https://github.com/rhysd/hello-github-actions/commit/c776e7146a031950fa579791f71448336a07880c" } ], "compare": "https://github.com/rhysd/hello-github-actions/compare/52875f0b1ed9...c776e7146a03", "created": false, "deleted": false, "forced": false, "head_commit": { "added": [], "author": { "email": "my-email@example.com", "name": "rhysd", "username": "rhysd" }, "committer": { "email": "my-email@example.com", "name": "rhysd", "username": "rhysd" }, "distinct": true, "id": "c776e7146a031950fa579791f71448336a07880c", "message": "check event.json", "modified": [ "action-a/entrypoint.sh" ], "removed": [], "timestamp": "2018-12-09T21:39:47+09:00", "tree_id": "7d4c96c54a304cd3af1bdbac684099bbbeb1dcc9", "url": "https://github.com/rhysd/hello-github-actions/commit/c776e7146a031950fa579791f71448336a07880c" }, "pusher": { "email": "rhysd@users.noreply.github.com", "name": "rhysd" }, "ref": "refs/heads/master", "repository": { "archive_url": "https://api.github.com/repos/rhysd/hello-github-actions/{archive_format}{/ref}", "archived": false, "assignees_url": "https://api.github.com/repos/rhysd/hello-github-actions/assignees{/user}", "blobs_url": "https://api.github.com/repos/rhysd/hello-github-actions/git/blobs{/sha}", "branches_url": "https://api.github.com/repos/rhysd/hello-github-actions/branches{/branch}", "clone_url": "https://github.com/rhysd/hello-github-actions.git", "collaborators_url": "https://api.github.com/repos/rhysd/hello-github-actions/collaborators{/collaborator}", "comments_url": "https://api.github.com/repos/rhysd/hello-github-actions/comments{/number}", "commits_url": "https://api.github.com/repos/rhysd/hello-github-actions/commits{/sha}", "compare_url": "https://api.github.com/repos/rhysd/hello-github-actions/compare/{base}...{head}", "contents_url": "https://api.github.com/repos/rhysd/hello-github-actions/contents/{+path}", "contributors_url": "https://api.github.com/repos/rhysd/hello-github-actions/contributors", "created_at": 1544355055, "default_branch": "master", "deployments_url": "https://api.github.com/repos/rhysd/hello-github-actions/deployments", "description": null, "downloads_url": "https://api.github.com/repos/rhysd/hello-github-actions/downloads", "events_url": "https://api.github.com/repos/rhysd/hello-github-actions/events", "fork": false, "forks": 0, "forks_count": 0, "forks_url": "https://api.github.com/repos/rhysd/hello-github-actions/forks", "full_name": "rhysd/hello-github-actions", "git_commits_url": "https://api.github.com/repos/rhysd/hello-github-actions/git/commits{/sha}", "git_refs_url": "https://api.github.com/repos/rhysd/hello-github-actions/git/refs{/sha}", "git_tags_url": "https://api.github.com/repos/rhysd/hello-github-actions/git/tags{/sha}", "git_url": "git://github.com/rhysd/hello-github-actions.git", "has_downloads": true, "has_issues": true, "has_pages": false, "has_projects": true, "has_wiki": true, "homepage": null, "hooks_url": "https://api.github.com/repos/rhysd/hello-github-actions/hooks", "html_url": "https://github.com/rhysd/hello-github-actions", "id": 161032314, "issue_comment_url": "https://api.github.com/repos/rhysd/hello-github-actions/issues/comments{/number}", "issue_events_url": "https://api.github.com/repos/rhysd/hello-github-actions/issues/events{/number}", "issues_url": "https://api.github.com/repos/rhysd/hello-github-actions/issues{/number}", "keys_url": "https://api.github.com/repos/rhysd/hello-github-actions/keys{/key_id}", "labels_url": "https://api.github.com/repos/rhysd/hello-github-actions/labels{/name}", "language": "Dockerfile", "languages_url": "https://api.github.com/repos/rhysd/hello-github-actions/languages", "license": null, "master_branch": "master", "merges_url": "https://api.github.com/repos/rhysd/hello-github-actions/merges", "milestones_url": "https://api.github.com/repos/rhysd/hello-github-actions/milestones{/number}", "mirror_url": null, "name": "hello-github-actions", "node_id": "MDEwOlJlcG9zaXRvcnkxNjEwMzIzMTQ=", "notifications_url": "https://api.github.com/repos/rhysd/hello-github-actions/notifications{?since,all,participating}", "open_issues": 0, "open_issues_count": 0, "owner": { "avatar_url": "https://avatars3.githubusercontent.com/u/823277?v=4", "email": "rhysd@users.noreply.github.com", "events_url": "https://api.github.com/users/rhysd/events{/privacy}", "followers_url": "https://api.github.com/users/rhysd/followers", "following_url": "https://api.github.com/users/rhysd/following{/other_user}", "gists_url": "https://api.github.com/users/rhysd/gists{/gist_id}", "gravatar_id": "", "html_url": "https://github.com/rhysd", "id": 823277, "login": "rhysd", "name": "rhysd", "node_id": "MDQ6VXNlcjgyMzI3Nw==", "organizations_url": "https://api.github.com/users/rhysd/orgs", "received_events_url": "https://api.github.com/users/rhysd/received_events", "repos_url": "https://api.github.com/users/rhysd/repos", "site_admin": false, "starred_url": "https://api.github.com/users/rhysd/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/rhysd/subscriptions", "type": "User", "url": "https://api.github.com/users/rhysd" }, "private": true, "pulls_url": "https://api.github.com/repos/rhysd/hello-github-actions/pulls{/number}", "pushed_at": 1544359187, "releases_url": "https://api.github.com/repos/rhysd/hello-github-actions/releases{/id}", "size": 3, "ssh_url": "git@github.com:rhysd/hello-github-actions.git", "stargazers": 0, "stargazers_count": 0, "stargazers_url": "https://api.github.com/repos/rhysd/hello-github-actions/stargazers", "statuses_url": "https://api.github.com/repos/rhysd/hello-github-actions/statuses/{sha}", "subscribers_url": "https://api.github.com/repos/rhysd/hello-github-actions/subscribers", "subscription_url": "https://api.github.com/repos/rhysd/hello-github-actions/subscription", "svn_url": "https://github.com/rhysd/hello-github-actions", "tags_url": "https://api.github.com/repos/rhysd/hello-github-actions/tags", "teams_url": "https://api.github.com/repos/rhysd/hello-github-actions/teams", "trees_url": "https://api.github.com/repos/rhysd/hello-github-actions/git/trees{/sha}", "updated_at": "2018-12-09T12:34:31Z", "url": "https://github.com/rhysd/hello-github-actions", "watchers": 0, "watchers_count": 0 }, "sender": { "avatar_url": "https://avatars3.githubusercontent.com/u/823277?v=4", "events_url": "https://api.github.com/users/rhysd/events{/privacy}", "followers_url": "https://api.github.com/users/rhysd/followers", "following_url": "https://api.github.com/users/rhysd/following{/other_user}", "gists_url": "https://api.github.com/users/rhysd/gists{/gist_id}", "gravatar_id": "", "html_url": "https://github.com/rhysd", "id": 823277, "login": "rhysd", "node_id": "MDQ6VXNlcjgyMzI3Nw==", "organizations_url": "https://api.github.com/users/rhysd/orgs", "received_events_url": "https://api.github.com/users/rhysd/received_events", "repos_url": "https://api.github.com/users/rhysd/repos", "site_admin": false, "starred_url": "https://api.github.com/users/rhysd/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/rhysd/subscriptions", "type": "User", "url": "https://api.github.com/users/rhysd" } }
アクション間で情報を受け渡しする
各アクションで毎回リポジトリが clone されるわけではなく,共通のワークスペースが使われます.なので前段のアクションで作成したファイルは後段のアクションでもアクセスすることができます.これによって,あるアクションでビルドした生成物を使って,後段で linter やテスト,デプロイを走らせるといったことができそうです.
サードパーティのアクションが後段にいる場合にはアクセストークンなどの秘密情報をファイルに保存しないように気をつける必要があります.試した限りでは環境変数は後段のアクションに受け継がれないので,前述の secrets 機能で環境変数に置いておくのが良さそうです.
Custom GitHub Action をつくる
アクションは別リポジトリや Docker コンテナに切り出して再利用することができます.actions organization に置かれている公式のアクション集が参考になります.
アクション1つのみを公開するとき
つくるリポジトリに対して公開するアクションが1つのときはリポジトリのルートにそのまま Dockerfile
と entrypoint.sh
を置きます
your-awesome-action/ ├── Dockerfile └── entrypoint.sh
使う側のリポジトリの .github/*.workflow
には uses
に owner/repo@ref
を指定すると使えるようになります.ref
はブランチ名か commit SHA1 の初め7桁を指定します(タグ名でも良い?).uses
は他にも docker://
で始めて直接 Docker コンテナを指定することもできるようです.
action "Awesome Action" { uses = "your-name/your-awesome-action@master" } workflow "hello" { on = "push" resolves = ["Awesome Action"] }
例: https://github.com/actions/npm
アクションを複数公開する時
1つのリポジトリで複数のアクションを公開したい時は,Dockerfile
と entrypoint.sh
をサブディレクトリに置きます.
your-awesome-action/ ├── action-a │ ├── Dockerfile │ └── entrypoint.sh └── action-b ├── Dockerfile └── entrypoint.sh
使う側のリポジトリの .github/*.workflow
には uses
に owner/repo/subdir@ref
を指定すると使えるようになります(ref
はアクション1つのみの場合と同じ).
action "Awesome Action A" { uses = "your-name/your-awesome-action/action-a@master" } workflow "hello" { on = "push" resolves = ["Awesome Action A"] }
例: https://github.com/actions/bin
感想
ざっと見た感じ,GitHub Action は下記の場合に便利そうです
- 開発のワークフローを自動化したいとき
- 自動でラベルを貼り替える
- 自動で issue を close する
- パッケージングしてデプロイしたりライブラリをリリースしたり
- ちょっとした CI を回したい
ただし下記の点は気をつけておいたほうが良さそうです
Rust プロジェクトで自動で Git hook をセットできる cargo-husky をつくった
npm でよく使っている husky というツールがあります.これは npm install
などを hook して Git hook を自動でセットしてくれるツールで,リモートにプッシュする前のチェックを強制してくれます.もちろん CI でもテストを回しているのですが,プッシュ前にもチェックすることによりケアレスミスに早く気付くことができます.
Rust でもこの仕組みを使いたかったのですが,そういうツールが無かった&つくれそうだったので cargo-husky というツールをつくりました.
基本的な使い方
cargo-husky
パッケージを Cargo.toml
の dev-dependencies
に追加して cargo test
を実行するだけです.
[dev-dependencies] cargo-husky = "1"
$ cargo test
cargo
はそのパッケージが必要になった段階でパッケージをダウンロードします. dev-dependencies なので cargo build
ではなく cargo test
を実行する必要があります.大抵開発中にテストを一度は実行すると思うので,意識して cargo test
を呼ぶ必要はなく,知らぬ間に Git hook がセットされているという意図です.
.git/hooks/
を見ると,こんな感じの pre-push
スクリプトが置かれているはずです.
#!/bin/sh # # This hook was set by cargo-husky v1.0.0: https://github.com/rhysd/cargo-husky#readme # Generated by script /path/to/cargo-husky/build.rs # Output at /path/to/target/debug/build/cargo-husky-xxxxxx/out # set -e echo '+cargo test' cargo test
これによって git push
前に cargo test
が自動で実行されるようになります.
フックをカスタマイズしたいとき
デフォルトでは pre-push
で cargo test
を実行するフックが置かれますが,cargo-husky
パッケージの feature flag を使ってこの挙動を変えることができます.
cargo book にもある通り,feature flag を指定するには [dev-dependencies.cargo-husky]
というセクションをつくります.
[dev-dependencies.cargo-husky] version = "1" default-features = false # デフォルトの挙動を無効化する features = ["precommit-hook", "run-cargo-test", "run-cargo-clippy"]
features
の配列に指定している値が有効にする機能です.この例では「コミット前に実行する(precommit-hook
)」,「cargo test
を実行する(run-cargo-test
)」,「cargo clippy
を実行する(run-cargo-clippy
)」機能を有効にすることにより,毎コミット前に cargo test
と cargo clippy
を実行する git hook pre-commit
が生成されます.
利用可能な feature flag は下記の通りです.
feature flag | 意味 | デフォルト値 |
---|---|---|
prepush-hook |
pre-push hook を生成 |
有効 |
precommit-hook |
pre-commit hook を生成 |
無効 |
postmerge-hook |
post-merge hook を生成 |
無効 |
run-cargo-test |
hook で cargo test を実行 |
有効 |
run-cargo-clippy |
hook で cargo clippy を実行 |
無効 |
user-hooks |
次の章を参照 | 無効 |
すでに生成された hook がある場合は,一旦 .git/hooks/
以下を削除してから cargo test
を実行してパッケージを再コンパイルしてください.
さらにフックをカスタマイズしたいとき
feature flag は固定値しか記述できないため,「自前で用意したスクリプトを実行したい」「cargo
に特定のオプションを渡して実行したい」といったカスタマイズはできません.
さらなるカスタマイズを可能にするために,user-hooks
という feature flag が用意されています.
[dev-dependencies.cargo-husky] version = "1" default-features = false features = ["user-hooks"]
この flag が有効になっていると,cargo-husky は自前で git hook を生成せず,リポジトリ直下に置かれている .cargo-husky
というディレクトリを探し,その中のスクリプトを代わりに .git/hooks
に配置します.
your-repository/ ├── .git └── .cargo-husky └── hooks ├── post-merge └── pre-commit
例えばディレクトリ構成がこのようになっているとき, pre-commit
および post-merge
が .git/hooks
以下に置かれる対象になります.
cargo-husky は .git/hooks
にスクリプトを置く際,ファイルの先頭にメタ情報(cargo-husky のバージョンなど)をヘッダとして挿入します.
その際,#
始まりを行コメントと想定しているため,それに準じた言語でスクリプトを書く必要があります.また,実行可能属性がついていないファイルはスクリプトとして認識せず無視します.なのでスクリプトには実行可能属性を付けておいてください(chmod +x
).これは意図しないファイルが .git/hooks
に置かれてしまわないようにするためです.
実装
husky は npm の install
フックなどで Git hook をセットしますが,cargo にはそういった仕組みはありません.
代わりに cargo の build script 機能を濫用することで cargo-husky は実装されています. 本来は外部ライブラリのビルドなどを設定するためのbuild.rs
内でフックをセットする処理を行っています.
cargo がビルド時に自動でセットする $OUT_DIR
に設定されたディレクトリを元にプロジェクトの .git
ディレクトリを特定するので,万一 $OUT_DIR
がリポジトリの外になっているような特殊なケースでは動きません.
cargo-husky は Linux/macOS/Windows で stable チャンネルのツールチェーンを使ってテストされており(Linux と macOS は Travis CI,Windows は Appveyor),MIT ライセンスで配布されています.
Vim に WebAssembly のテキストフォーマットのサポートを入れた
Vim に WebAssembly のテキストフォーマット (wast) の対応を入れ,同時に filetype=wast
で使われるファイルのメンテナになりました.
Wasm のテキストフォーマットとは
WebAssembly にはバイナリ形式とテキスト形式の2つのフォーマットがあります.Wasm はネットワークを介して配布される前提のため,サイズの小さいバイナリ形式のほうが良いですが,デバッグなどで処理を追いたい人間にとってはテキストフォーマットも必要になるためです.
emscripten はデバッグオプションをつけてコンパイルすると,生成物 .wasm
のほかにテキストフォーマットの .wast
およびそのソースマップを生成してくれます.
こんな感じのS式です
contribution の流れ
0. Vim プラグインとして実装
まだブラウザが Wasm を初めてサポートし始めた頃,その頃の binaryen の出力を眺めるときにハイライトが無いと不便なので,まずはたたき台として vim-wasm をつくりました.
今年の6月に入って WebAssembly の仕様をざっくり眺めて vim.wasm をつくりました.再び emscripten の出力を見る機会があり,以前つくったものが大分間違っていたことが分かったので,仕様に従って vim-wasm をそれに従って大幅に修正しました.
1. Vim にプルリクを出す
WebAssembly は主要な各ブラウザが対応しており,ウェブ標準として定義されているので,テキストフォーマットの対応が Vim 本家に入っていると有意義だと考えました.
vim-wasm が良い感じに .wast
ファイルをサポートできていることが確認できたので,Vim に取り込んでもらうことを提案します.
Vim は vim_dev にパッチを送るか,vim/vim にプルリクを作成することでレビューを依頼できます.通常,runtime/syntax
や runtime/indent
といった各 filetype 対応のファイルにはメンテナがそれぞれついているので,まずはそちらに依頼すべきですが,今回は新規追加なので直接 Bram にレビューしてもらいました.
2. master に取り込んでもらう
レビューで OK が出ると,Bram が master ブランチに取り込みます.
今回は runtime/
以下のファイル追加なので,他の runtime/
向けの変更もまとめて1つのパッチが作成され,master
ブランチに追加されました.
3. filetype=wast
のメンテナになりました
先程書いたように,各 filetype のサポートはそれぞれにメンテナがいます.
そんなわけで,僕も vim/runtime/{syntax,indent,ftplugin}/wast.vim
のメンテナになりました.開発は引き続き vim-wasm のほうでやっていくので,何か問題を発見された際は,Vim のリポジトリや vim_dev ではなく,まずはvim-wasm のほうに issue やプルリクをつくっていただけると助かります.