Rust 公式 linter の clippy に新しいルールを実装した
Rust 公式の linter,clippy に新しいルールを足すプルリクを出してマージされた時のメモです.
dbg!
マクロ
Rust 1.32 で dbg!
というマクロが追加されました.
これは値を1つ引数にとってその値を返すマクロで,受け取った値とソースコード上での位置を print します.
fn factorial(n: u32) -> u32 { if dbg!(n <= 1) { dbg!(1) } else { dbg!(n * factorial(n - 1)) } } dbg!(factorial(4));
名前の通り,いわゆる print debug 用途のマクロなので,デバッグが終わってリポジトリに commit する前にはコードから dbg!
マクロを削除しておくのがベストプラクティスです.
公式ドキュメントにも
Note that the macro is intended as a debugging tool and therefore you should avoid having uses of it in version control for longer periods.
と書いてあります.
ちょっとしたデバッグに非常に便利なのですが,値を move で受け取ってその値を返すだけなのでうっかり消し忘れるとテストを壊すこともなく気付けないケースがあることに気付きました.
dbg_macro
ルール
そこで,コード内の dbg!
マクロを検出できる dbg_macro
ルールをつくりました.
#![deny(clippy::dbg_macro)] fn main() { dbg!(42); }
のようなコードに対して,
cargo clippy -- -W clippy::dbg_macro
のように実行すると
error: `dbg!` macro is intended as a debugging tool --> foo.rs:4:5 | 4 | dbg!(42); | ^^^^^^^^ | help: ensure to avoid having uses of it in version control | 4 | 42; | ^^
のように警告を出します.
デフォルトで有効になっているとデバッグ中にエディタが警告しまくってうるさいので,restriction
カテゴリでデフォルトは無効になっています.
restriction
カテゴリは README には載っていませんが,「unimplemented!
を使わない」や今回のルールのような特定の場面(production 前のチェックなど)で有効なルールや,「mem::forget
を Drop
を実装した型で使わない」といった万人向けではないキツめのルールが登録されています.
clippy のリポジトリを restriction で検索するとざっと見渡すことができます.
新しいルールの提案と追加
まずは issue で「こういうルールあると良いと思う」と提案し,特に反対もなくメイン開発者の upvote も付きました.'good first issue'(初めて contribute する人が取り組むと良い易しい issue)のラベルがついたので,自分で実装してみて,プルリクを出しました.あとは一般的なプルリクと同じで何度かレビューしてもらって OK が出てマージという流れでした.
clippy の実装
基本的にはまず CONTRIBUTING.md を読めば大体分かるようになっており,必読です.
ディレクトリ構成
src/*
: clippy のコマンドライン部分とドライバ(実行の前段部分)の実装のみclippy_lints/
: linter の各ルールの実装clippy_lints/lib.rs
: ルールすべてを import して登録しているところclippy_lints/*.rs
: ルールの実装(lib.rs
以外)clippy_lints/utils
: ルールの実装に使うあれこれ
clippy_dev/
: 開発時に使うツール群(ルールリストの更新など)clippy_dummy/
: テスト向けtests/
: pass を走らせる部分のテストや各ルールを適用して正しく警告が出るかどうかのテスト
新しいルールを足すには基本的に clippy_lints
crate および tests/
に追加・修正を加えることになります.
Early Pass と Later Pass
clippy のルールは構文木(syntax::ast
)もしくは HIR(rustc::hir
)に対する pass として実装します.HIR はパースした構文木にコンパイラ向けの情報を足したもので,Rust RFC 1191に詳しく書いてあります.
自前の pass を実装して登録しておくと,clippy が構文木または HIR をトラバースしたときに各ノードにその pass を適用して,pass に実装したコールバックメソッドがマッチすると適宜呼び出されます.
構文木にマッチさせる pass を early pass,HIR にマッチさせる pass を later pass といい,下記のような順序で適用されます.
early pass は構文木のみ,later pass は HIR,型情報,コンパイラコンフィグ(cfg!
)などにアクセスできます.
構文木をパースしたり pass を適用するなどの処理は rustc コンパイラ本体に linter 向けの汎用的な API があり(rust/src/librustc/lint
),rustc コンパイラ自体の unused 警告などもこれを利用しています(rust/src/librustc_lint
).rust-clippy リポジトリではそれを利用してルール集とコマンドライン部分のみを実装しています.なので,clippy の実装時には rustc
,rustc_*
や syntax
といった rustc コンパイラ API を知る必要があります.
具体的に early pass と later pass でどのパスを通しているかは rust/src/librustc/lint/mod.rs
に実装があります.
https://github.com/rust-lang/rust/blob/master/src/librustc/lint/mod.rs
新しい Early Pass を追加する
まず clippy_lints/src
内にルール向けのソースファイルを作成します.ここでは clippy_lints/src/my_rule.rs
としたとします.
declare_clippy_lint! { pub MY_RULE, style, "my rule for clippy" }
第1引数が linter インスタンス,2引数目が perf, correctness, complexity, style などのカテゴリです.
declare_clippy_lint
は rustc::lint
が提供する declare_tool_lint!
マクロの薄い wrapper になっていて,カテゴリを見てデフォルトの警告レベルをセットするなどの設定を行っています.
次に pass オブジェクトを定義します.
#[derive(Copy, Clone, Debug)] pub struct MyRule; impl LintPass for MyRule { fn get_lints(&self) -> LintArray { lint_array!(MY_RULE) } fn name(&self) -> &'static str { "MyRule" } } impl EarlyLintPass for MyRule { fn check_expr(&mut self, cx: &EarlyContext<'_>, e: &syntax::Expr) { // ここにチェック処理を実装 } }
rustc::lint::LintPass
の実装ではその pass の共通情報を記述します.1つの pass で複数のルールをチェックすることができ,rustc::lint::lint_array!
マクロで指定します.
early pass の実装本体は rustc::lint::EarlyLintPass
を実装することで実装します.もともと EarlyLintPass
にある check_*
メソッドをオーバーライドすると,そのメソッドが対応する構文木ノードを visit したときに適用されます.rustc::lint::EarlyContext
を通じて lint のコンテキスト情報を受け取ることができます.
上記では式に対する処理 check_expr
をオーバーライドしていますが,一覧はrustc のソース内で確認できます.
構文木のパターンがチェックしたいパターンにマッチしているかをチェックし必要な情報を抜き出すには match
や if let
などのパターンマッチをネストさせまくることになるので,if_chain::if_chain!
マクロが便利です.
例えば let x = EXPR; x
を取り出す処理はこんな感じに書けます.
if_chain! { if let Some(retexpr) = it.next_back(); if let ast::StmtKind::Expr(ref retexpr) = retexpr.node; if let Some(stmt) = it.next_back(); if let ast::StmtKind::Local(ref local) = stmt.node; if let Some(ref initexpr) = local.init; if let ast::PatKind::Ident(_, ident, _) = local.pat.node; if let ast::ExprKind::Path(_, ref path) = retexpr.node; if !in_external_macro(cx.sess(), initexpr.span); then { // ここでマッチしたときの処理 } }
警告すべきコードをパターンマッチで見つけたら警告を出す処理を書きます.警告を出す方法は clippy_lints/src/utils
に便利関数群があり,
- 警告メッセージのみ:
span_lint()
,span_lint_node()
- 警告メッセージとヘルプ:
span_help_and_lint()
- 警告メッセージとノート:
span_note_and_lint()
- 警告メッセージとヘルプと修正提案:
span_lint_and_sugg()
などが使えます.span とはソースコード片のことで,開始位置・終了位置・コンテキスト情報がエンコードされた u32
の値で,ソースコードのうち警告を出す部分を指定するのに使えます.各構文木ノードは syntax::Spanned
で wrap されて check_*
メソッドに渡されるので,ノードに対応する span は簡単に取得できます.
span_lint_and_sugg()
ではソースコードの修正提案を String
で渡せます.rustc_errors::Applicability::MachineApplicable
を指定することで,自動修正機能(おそらく rustfix?)で自動修正できます.渡した文字列で span で指定した範囲を置き換えます.span は utils::snippet()
を使ってコード片(スニペット)として文字列化することができ,修正提案のための文字列が楽につくれるケースがあります.
最後に作成したルールを clippy に登録します.clippy_lints/src/lib.rs
内で下記のように対応するカテゴリに linter 情報を追加します.
// ... pub mod my_rule; // ... reg.register_lint_group("clippy::style", Some("clippy_style"), vec![ // ... my_rule::MY_RULE // ... ]); // ...
最後につくった pass を登録するのですが、early pass の場合はここで注意が必要です。
// マクロ展開後で OK な pass は普通に early pass として登録 pub fn register_plugins(reg: &mut rustc_plugin::Registry<'_>, conf: &Conf) { // ... reg.register_early_lint_pass(box reference::Pass); // ... } // マクロに対する lint など,マクロの展開前でないと動かない pass はこっちに登録 pub fn register_pre_expansion_lints(...) { // ... store.register_pre_expansion_pass(Some(session), true, false, box dbg_macro::Pass); }
マクロの構文木ノードにマッチさせる check_mac()
などを使う場合は後者の register_pre_expansion_pass
側に pass を登録する必要があります.
early pass の適用順序は
pre_expansion_pass
- マクロの展開処理
early_lint_pass
となっています。
新しい Later Pass を追加する
early pass と基本的には同じで,rustc::lint::EarlyLintPass<'a>
の代わりに rustc::lint::LateLintPass<'a, 'tcx>
を実装します.tcx
は型情報のコンテキストの寿命を表しているようです.
impl<'a, 'tcx> LateLintPass<'a, 'tcx> for MyRule { fn check_expr(&mut self, cx: &LateContext<'a, 'tcx>, expr: &'tcx rustc::hir::Expr) { // ルールの実装 } }
オーバーライドできるメソッドはrustc のソース内で確認できます.
early pass と違い,check_expr
などのメソッドに rustc::hir::*
が渡され,型情報のコンテキストの寿命 tcx
が制約として付きます.
型情報には LateContext
の tcx
フィールドからアクセスできます.(e.g. cx.tcx.fn_sig
)コンパイラコンフィグ(cfg
)は cx.tcx.sess.parse_sess.config
にある rustc::session::config::Config
な値にアクセスすることでチェックできるようです.構造体のサイズなどターゲット依存で変わるものをチェックする際に使われます.
また,later pass では call flow graph も rustc::cfg::CFG
を使って取得できます.
impl<'a, 'tcx> LateLintPass<'a, 'tcx> for MyRule { fn check_fn( &mut self, cx: &LateContext<'a, 'tcx>, kind: intravisit::FnKind<'tcx>, decl: &'tcx FnDecl, body: &'tcx Body, span: Span, node_id: NodeId, ) { // call flow graph を生成 let cfg = CFG::new(cx.tcx, body); // ... } }
例えば cyclomatic complexity を計算する際に使われているようです.
pass の登録について,later pass の場合はマクロに対するテストかどうかで pass の登録先を分ける必要はありません.ある HIR ノードがマクロ展開結果生成されたものかは,そのノードの span を使って clippy_lints/src/utils
の utils::is_expn_of
で分かります.
テストの実装
テストは UI テスト(問題があるコードに実装したルールを適用して,結果として期待する警告の出力がコマンドライン出力として得られるかどうか)のみで行います.
CONTRIBUTING.md によると,実装前にまずは警告を出してほしいコードを tests/ui/my_rule.rs
に書き,TESTNAME=ui/my_rule cargo test --test compile-test
と実行すると,そのコードの構文木にマッチする linter 実装コードの雛形をつくってくれるらしいのですが,僕の場合は dbg!
が展開された後の構文木にマッチするようなコードが吐かれてしまったため使えませんでした.
linter の実装が終わったら CLIPPY_TESTS=true cargo run --bin clippy-driver -- -L ./target/debug tests/ui/my_rule.rs
で警告が意図通り出力されていることを確認し,その結果を多少整形して tests/ui/my_rule.stderr
として保存します.最後に TESTNAME=ui/my_rule cargo test --test compile-test
で my_rule
向けのテストを実行して結果を確認できます.
dbg_macro
ルールの実装
今回実装したルールは dbg!(expr)
マクロの使用箇所を検知して警告として表示し,dbg!(expr)
の代わりに expr
を修正提案として表示する小さなものです.
rustc::lint::EarlyLintPass
として実装し,pre-expansion pass として登録します.マクロ呼出しにマッチする EarlyLintPass::check_mac
メソッドをオーバーライドして実装し,渡された ast::Mac
のパスが "dbg"
かどうかチェックするだけです.
impl EarlyLintPass for Pass { fn check_mac(&mut self, cx: &EarlyContext<'_>, mac: &ast::Mac) { if mac.node.path == "dbg" { // 警告を表示 } } }
ast::Mac
は syntax::Spaned
で wrap された型なので mac.span
でマクロ呼出しのソースコード上での範囲(span)は簡単に取得できます.
あとは修正提案用の文字列を生成できれば終わりです.
マクロは引数にトークン列を取るので,展開前は引数のトークン列のみが構文木ノードに格納されています(mac.node.tts
).
dbg!(expr)
マクロの中身 expr
の span が取得できれば,その範囲を utils::snippet
でコード片化できます.
引数の span を取る方法は
- トークン列の最初のトークンの span の始まり位置とトークン列の最後のトークンの span の終わり位置からトークン列全体の範囲を表す span を生成する
dbg!()
は仕様として1つの値を取るので,トークン列をsyntax::ast::Expr
にパースした結果のノードから span を取り出す- early pass をやめ,later pass にして
dbg!()
の引数部分から展開された HIR ノードを特定して span を取り出す
のざっくり3通りが考えられます.3. は大変そうだし 2. は式としてパースできなかった場合のエラーハンドリングやパースのコストがあるので,1. が一番良さそうです.
Rust のトークン列は syntax::tokenstream::TokenStream
という型で表され,syntax::tokenstream::TokenTree
の列で表現されています.どうやらこれらは proc macro の実装などで使う proc_macro::TokenStream
や proc_macro::TokenTree
とは別物のようです.
TokenStream
は .trees()
で(clone した)TokenTree
のイテレータを返せるので,そこから最初のトークンツリーと最後のトークンツリーを取得し,Span::to
を使って最初のトークンツリーの span から最後のトークンツリーの span までの範囲を表す span を新たに生成します.
let mut cursor = mac.node.tts.trees(); let first = cursor.next().unwrap(); let last = cursor.last().unwrap_or(first); let entire_span = first.span().to(last.span());
最後にスニペット文字列を span から生成すれば修正提案に使う文字列が生成できます.
let snip = utils::snippet_opt(cx, entire_span).unwrap().to_string();
最後に clippy_dev
と cargo fmt
を実行しておきます.clippy_dev
は clippy_lints/src/lib.rs
で登録されている pass をチェックしたり,CHANGELOG.md と README.md を更新したりしてくれます.
cd clippy_dev/ cargo run -- update_lints cargo +nightly fmt --all
まとめ
dbg_macro
ルールを Rust 公式 linter の clippy に追加しました.次の clippy のリリース時に入ると思うので,良ければ CI や Git の pre-commit もしくは pre-push フックなどで
cargo clippy -- -W clippy::dbg_macro
と実行して使ってみてください.
また clippy に新しいルールを追加する方法についても簡単に紹介しました.rustc
,rustc_*
,syntax
あたりの nightly でしか使えない rustc コンパイラ API を使ったり読んだりする必要はなかなか無いので,実際使ったのはごく一部ですが良い機会だったと思います.ちなみにこれらの crate は rustc コンパイラの内部実装で頻繁に変更されるので,上記の紹介した内容もいずれ正しくなくなる可能性が高いです.
Vim の構文ハイライトでクリスマスツリー🎄を飾ってメリクリする
Vim Advent Calendar 2018 の24日目の記事です.昨日は Kaoriya さんのVim に VOICEROID で喋らせたでした.
もうすぐクリスマスなので,クリスマスツリーを飾りたいと思います.ただ飾るだけだと Vim のネタにならないので,Crystal や Wast,vim-gfm-sytnax, vim-github-actions といったファイルタイププラグインをつくった経験を活かし,構文ハイライトを使って Vim の中でクリスマスツリーを飾っていきたいと思います.
Vim の構文ハイライトについての情報は下記の help ドキュメントを読んでいただければ,ある程度網羅的な情報が手に入るので,実際に何かのファイルタイプを追加するプラグイン(ファイルタイププラグイン)を実装する時は,まずざっと一通りドキュメントを眺めることをおすすめします.
この記事はまだファイルタイプを書いたことが無い人を対象とします.網羅的な情報ではなく,ハイライトをつけていく過程を実例とともにコードで説明して,ファイルタイププラグインを実装する大まかな手順を把握していただくのを目的として進めていきます.今回はインデントについては時間の都合(や題材の都合)で省略します.
今回使ったコードはすべてこのリポジトリにあります.
ファイルタイププラグインとは
本題に入る前にファイルタイププラグインを知らない方向けにざっくりとした説明をしておきます.
特定の拡張子のファイルを開いたときなど,set filetype=...
のようにファイルタイプをセットしたときにそのファイルタイプの構文ハイライトやインデント設定などを行ってくれるプラグインをファイルタイププラグインと呼びます(:help filetype-plugin
).
Vim は c
や vim
をはじめ,デフォルトで多くのファイルタイプをサポートしていますが,自分で新しいファイルタイプを足すこともできます.有名どころだと typescript-vim や vim-toml など無数にあります.
また,vim-css3-syntax や vim-jsx-pretty, vim-gfm-syntax のように,自前でファイルタイプを定義するのではなく,既存のハイライトに独自のハイライトを足すようなプラグインもあります.
下記のような場合にファイルタイププラグインを自作する必要があります
- Vim が公式で対応していない言語のコードをハイライトしたい(e.g. 自作言語,自作設定ファイル)
- 既存のファイルタイププラグインではうまく動かない(or メンテされていない)などの事情で,自分でつくりたい
- アプリのログを読むときに重要な箇所をハイライトしたい
0. まずは対象の仕様を把握する
ファイルタイププラグインを書き始める前に,まずは対象を把握します.特定の言語のファイルタイププラグインをつくる場合は,まずはその言語の構文の仕様を眺めたりします.例えば vim-wasm をつくる際には WebAssembly の Text Format の仕様を読んだりしました.
今回はクリスマスなので,下記のクリスマスツリーをハイライト(飾り付け)していきたいと思います.
asciiart.eu の Laura T さんの作品をベースにしてつくってみました.
* ☆ * * .o * * * .o.'. .'.'o'. * * o'.*.'.o. * .'.o.'.'.*. * * * .*.'.o.'.o.'. * * o'.'.'.'*'.'.'. * * .'.'*'.'o'.'.'*'o * * [_____] * * \___/ * * Merry Christmas!
ツリーのてっぺんには☆が飾られており,o
や *
で装飾された木だと思ってください.周りの *
はクリスマスなので雪が降っています.このままでも綺麗ですが(?),色を付けて飾りつけていきましょう.
新しいファイルタイプとして christmastree
を定義することにします.このファイルタイプが定義されている時は Vim はバッファにクリスマスツリーが描かれているとしてハイライトするようにこれから実装していきます.
1. リポジトリとファイルタイプの認識
まずは Vim プラグインのリポジトリを用意します.ディレクトリを runtimepath
に直接追加する(.vimrc
内で set rtp+=/path/to/vim-syntax-christmas-tree
)か,お好みのプラグインマネージャでロードするプラグインのリストに追加してください.
mkdir vim-syntax-christmas-tree && cd vim-syntax-christmas-tree git init .
ディレクトリ構成はこのようになっています. christmas-tree.txt
は上記のアスキーアートが描かれたテキストファイルです.
vim-syntax-christmas-tree/ ├── christmas-tree.txt ├── ftdetect/ │ └── christmastree.vim ├── syntax/
まずはファイルタイプを認識させます.Vim は ftdetect/*.vim
の中身を起動時にロードしてくれるので,ftdetect/christmastree.vim
をつくり,この中にファイルタイプを認識するための :autocmd
を書きます.
autocmd BufNewFile,BufReadPost christmas-tree.txt setlocal filetype=christmastree
今回は christmas-tree.txt
という固定のファイル名のファイルを読んだときのみ,ファイルタイプ christmastree
をセットするようにしています.実際は TypeScript なら *.ts
のように拡張子でパターンを指定することが多いです.
ここで一旦 Vim を新たに開き,christmas-tree.txt
を開いて :set ft
で今のファイルタイプが christmastree
になっているのを確認します.
2. 構文ハイライトファイルをつくる
Vim は構文ハイライトファイルとして syntax/{filetype}.vim
か syntax/{filetype}/*.vim
を探すようになっているので,syntax/christmastree.vim
をつくります.
まだ何もハイライトしない空の状態です.
if exists("b:current_syntax") finish endif " ここにハイライト処理の実装を書いていく let b:current_syntax = "christmastree"
変数 b:current_syntax
はそのバッファをハイライトしている構文の名前です.すでにハイライト済みかどうかはその変数があるかどうかでチェックし,まだハイライトされていなければハイライト処理を行うという構造になっています.ハイライトを行った後は,すでにハイライトしたということが分かるように,b:current_syntax
を定義しておきます.
3. はじめてのハイライト: :syn match
まずはてっぺんの☆をハイライトしてみます.
これは単に ☆
という文字列を色をつけてハイライトすれば良さそうです.
syn match christmastreeStar "☆" hi def link christmastreeStar Identifier
:syn match {ハイライトグループ} {パターン}
を使っています.これにより,指定したパターン(正規表現)にマッチする文字列を指定したハイライトグループでハイライトできます.
ここでは christmastreeStar
というハイライトグループをつくることにし,"☆"
というパターン(正規表現)にマッチする箇所とそのハイライトグループを紐づけます.この時点ではハイライトグループを紐づけただけなのでまだ色はつきません.ハイライトグループ名は小文字始まりのファイルタイプ名をプレフィックスとしてつけます.これはハイライトグループ名が一意でないといけない(他とかぶると上書きされてしまう)ためです.
次の行の hi def link {ハイライトグループ} {別のハイライトグループ}
という行で,christmastreeStar
を Identifier
というハイライトグループに紐づけています.
Vim はデフォルトでいくつかのハイライトグループを定義しています.例えば (Normal
: 通常のテキスト,Visual
: ビジュアルモード選択 など).また,習慣として,同じ構文を同じグループにまとめたものが推奨されるハイライトグループ(Identifier
: 変数名,Statement
: 文 など)として明記されています.いずれも :help :hi
で一覧を確認できるので,ざっと見ておいたほうが良いです.カラースキームプラグインはこれらの推奨ハイライトグループやデフォルトのハイライトグループに対して色を指定することで好きな色合いを実現しています.(カラースキームについては,もしよければフルスクラッチからさいきょうの Vim カラースキームをつくろう!を参照してみてください)
Vim で :hi
と打つと今のハイライトグループとそれに指定されている色がリストで表示されるので,そこで実際の色を確認できます.
:hi def link
はデフォルトでハイライトグループを別のハイライトグループにリンクします.なので,ここでは christmastreeStar
というハイライトグループにマッチするテキストは Identifier
と同じ色でハイライトされます.
実際にプログラミング言語のハイライトを書く場合は,対象の構文が何にあたるかでリンクするハイライトグループを決めます.例えば if
文なら Statement
,関数定義なら Function
など.今回は残念ながら(?)クリスマスツリーに適した推奨ハイライトグループが無いため,適当に手元のカラースキームで合いそうな色を選択しました.
ちなみに
syn match Identifier "☆"
のように新しいハイライトグループを定義せず直接共通のハイライトグループを指定することもできますが,カラースキームがハイライト色を上書きできなくなるためやめておいたほうが良いです.:hi def
はすでにハイライトがリンクされているときに何もしないので,カラースキーム側で先に :hi link
してやることで,ハイライト色を上書きできるようになります.
実際に Vim で開いて確認してみます.:term ++close vim christmas-tree.txt
とすることで,Vim の中で直接 Vim を開いて最新のハイライトを確認できます.
やりました! ☆
がハイライトされていることが分かります.
ちなみに本来は短い特定の文字列は :syn keyword
でキーワードとしてハイライトするのが一般的ですが,:syn keyword
は 'iskeyword'
に指定されている文字でないとハイライトしてくれないので,☆
には使えず :syn match
を使いました.
4. 葉をハイライトする: :syn region
次にツリーの葉を緑でハイライトしてみます.先程の :syn match
を使っても良いですが,今回は :syn region
を使ってハイライトします.周りには雪が舞っていてそれらは葉に含めたくないので,行ごとにハイライトするのが良さそうです.
syn region christmastreeLeaves start=/\s\@<=[o.]/ end=/[o.]\%(\s\|\_$\)\@=/ oneline hi def link christmastreeLeaves String
新たなハイライトグループ christmastreeLeaves
を定義しています. syn region
はハイライトするテキストの始め(start=
)と終わり(end=
)によってハイライトする範囲を指定できます.始まりと終わりがダブルクォートな文字列リテラル("..."
)や,{...}
によるブロック構文などは :syn region
を使うことが多いです.
ここでは,木は .
か o
で始まり,.
か o
で終わっていることに目をつけて,空白の後に .
か o
が続く場合を始めとし,空白または改行に続く .
か o
を終端とします.
最後に,葉は行ごとにハイライトすると決めたので,oneline
オプションを指定しておきます.これによって,ハイライトが次の行に継続してハイライトされることがなくなります.
実際に Vim で開いて確認してみます.
無事,意図通り緑色(私のローカルのカラースキームでは文字列が緑なので String
を指定している)でハイライトされています.少し木っぽくなってきた気がします.
5. ハイライトをネストさせる: contained
, containedin=
, contains=
飾り *
をハイライトしてみます.☆
のとき同様に *
にマッチするハイライトを新しいハイライトグループ christmastreeGlitter
で指定し,
syn match christmastreeGlitter /\*/ hi def link christmastreeGlitter Constant
実際に Vim で開いて確認してみます.
雪のほうがハイライトされてしまいました.一方でハイライトしたほうの木の *
のほうはハイライトされていません.これは,葉のハイライトのほうが優先されてしまっているためです.
ここでやりたいことは,葉の中の *
だけをハイライトするということです.
これはハイライトを「ネストさせる」ことで実現できます.Vim のハイライトは正規表現でマッチする領域を重ねた層のような構造になっており,contains=
, containedin=
, contained
などの :syn
のオプションでそれらを制御します.
今回の例では
syn region christmastreeLeaves start=/\s\@<=[o.]/ end=/[o.]\%(\s\|\_$\)\@=/ contains=christmastreeGlitter oneline keepend syn match christmastreeGlitter /\*/ contained containedin=christmastreeLeaves hi def link christmastreeLeaves String hi def link christmastreeGlitter Constant
のようにすることで実現できます.
contained
をつけたハイライトはトップレベルではハイライトされなくなり,後述の contains=
でネストしてマッチした場合のみハイライトされるようになります.これによって,外の雪は白いままになります.
ハイライトをネストさせるには,別のハイライトを含む側に contains=
で含むハイライトのグループ名をコンマ区切りで書き,含まれる側のハイライトに含むハイライトグループを containedin=
で指定します. containedin=
は無くても良いですが,あったほうが意図しないハイライトを防げて良いと思います.
実際に Vim で開いて確認してみます.
無事,木の飾りの *
だけに色をつけることができました.
実際のファイルタイププラグインでは,このようにハイライトをネストさせることで特定の構文のみに適用されるハイライトを定義します.例えば if
文が関数ブロックの中でしか書けないような言語であれば,関数ブロック全体にマッチするハイライトを定義し,その中にネストして if
文のハイライトをマッチさせるような実装が考えられます.
ちなみに,しれっと keepend
というオプションを足していることに気付いたでしょうか?:syn region
は同じ構文でネストしたハイライトをうまく扱う(例えば { { ... } }
のようにネストしていても内側と外側にうまくハイライトマッチできる)ような挙動になっているのですが,これだと外側(christmastreeLeaves
)のマッチするテキストの末尾で内側(christmastreeGlitter
)が同時にマッチするとき,内側が優先されて外側のマッチが終了しなくなります.この挙動を変えるのが keepend
で,内側のハイライトと外側のハイライトが同時に終了するようになります.
6. 複数のハイライトをまとめる
木の飾りは *
と o
の2種類あります.それぞれに違う色を割り当てられたほうが華やかになるので,o
は *
とは別のハイライトを割り当てます.
ここで先程のセクションと同様にして o
もハイライトしても良いですが,せっかくなので1つの「クラスタ」にまとめることにします.
複数のハイライトグループをひとまとめにする :syn cluster
を使います.
syn cluster christmastreeGlitters contains=christmastreeGlitterSmall,christmastreeGlitterLarge syn region christmastreeLeaves start=/\s\@<=[o.]/ end=/[o.]\%(\s\|\_$\)\@=/ contains=@christmastreeGlitters oneline keepend syn match christmastreeGlitterLarge /o/ contained containedin=christmastreeLeaves syn match christmastreeGlitterSmall /\*/ contained containedin=christmastreeLeaves hi def link christmastreeLeaves String hi def link christmastreeGlitterSmall Constant hi def link christmastreeGlitterLarge Special
まずは *
を christmastreeGlitterSmall
,o
を christmastreeGlitterLarge
として別のハイライトグループにし,それぞれに別のハイライト色を指定できるようにします.
次に :syn cluster {クラスタ名} contains={コンマ区切りのハイライトグループ}
を使って @christmastreeGlitters
をひとまとめにします.
クラスタは :syn region
の contains=
のように,コンマ区切りで複数のハイライトグループを指定する箇所で使えます.
実際に Vim で開いて確認してみます.
今回の例では,まとめずに *
と o
で別個にハイライトを指定するのに比べてあまり嬉しさが分かりませんが,数が増えたり,階層構造が深くなったりしたときに管理がかなり楽になります.
実際のファイルタイププラグインでは,例えば関数ブロックの中に含められるハイライトグループをひとまとめにして1つのクラスタにしたりといった管理をしているのを見ます.vim-wasmでは,WebAssembly のテキストフォーマットはS式で記述するので,(...)
で囲まれた中のみでハイライトが効く(逆に括弧の外であるトップレベルではコメント以外ハイライトされない)ように,括弧の中だけでハイライトすべきハイライトグループを1つのクラスタにしています.
7. 完成
最後に忘れていた植木鉢を :syn match
でハイライトさせて完成です.[___]
と \___/
に分けてマッチさせていますが,それぞれ同じ構文の一部として同じハイライトグループ名 christmastreeFlowerpot
を指定しています.
このように,1つのハイライトグループを複数の :syn
で構成することもできます.
これでようやく一通り飾り終えることができました. syntax/christmastree.vim
の全体像は下記の通りです.
if exists("b:current_syntax") finish endif syn cluster christmastreeGlitters contains=christmastreeGlitterSmall,christmastreeGlitterLarge syn match christmastreeStar /☆/ syn region christmastreeLeaves start=/\s\@<=[o.]/ end=/[o.]\%(\s\|\_$\)\@=/ contains=@christmastreeGlitters oneline keepend syn match christmastreeGlitterLarge /o/ contained containedin=christmastreeLeaves syn match christmastreeGlitterSmall /\*/ contained containedin=christmastreeLeaves syn match christmastreeFlowerpot /\[_\+\]/ syn match christmastreeFlowerpot /\\_\+\// hi def link christmastreeStar Identifier hi def link christmastreeLeaves String hi def link christmastreeGlitterSmall Constant hi def link christmastreeGlitterLarge Special hi def link christmastreeFlowerpot PreProc let b:current_syntax = "christmastree"
実際に Vim で開いて確認してみます.
無事綺麗に飾りつけられました.お疲れ様でした.
8. さらにピカピカさせる
構文ハイライトだけでも綺麗ですが,ftplugin
を使ってさらに華やかにしてみます.
Vim はファイルタイプをセットしたときに ftplugin/{filetype}.vim
または ftplugin/{filetype}/*.vim
を読みに行くため,そのファイルの中にコードを書いておけば,特定のファイルタイプをセットしたときに処理を行うことができます.
今回は ftplugin/christmastree.vim
を作成し,構文ハイライトの b:current_syntax
のように,もう ftplugin
を読み終えたことを示す b:did_ftplugin
をチェックする処理を最初に書きます.これにより,意図せず複数の ftplugin
スクリプトが読まれてしまうことを防ぎます.
こうすることで,例えば別の ftplugin
を作成して既存の ftplugin
を置き換えると言ったことができるようになります.
if exists("b:did_ftplugin") finish endif let b:did_ftplugin = 1
今回はタイマーを使って,一定時間ごとに飾りの色が変わるようにしてみました.
function! s:christmas_glitter_tick(timer) abort let small = get(b:, 'christmastree_glitter_small_colors', ['Constant', 'Statement', 'Special', 'Ignore']) let large = get(b:, 'christmastree_glitter_large_colors', ['Special', 'Keyword', 'Identifier', 'Normal', 'Statement', 'Constant', 'Ignore']) let idx = s:tick % len(small) execute 'hi link christmastreeGlitterSmall' small[idx] let idx = s:tick % len(large) execute 'hi link christmastreeGlitterLarge' large[idx] let s:tick += 1 endfunction function! s:christmas_glitter_start() abort if exists('s:timer_id') return endif let s:tick = 1 let s:timer_id = timer_start(1000, function('s:christmas_glitter_tick'), {'repeat': -1}) endfunction function! s:christmas_glitter_stop() abort if !exists('s:timer_id') return endif call timer_stop(s:timer_id) unlet! s:timer_id unlet! s:tick endfunction if get(g:, 'christmastree_glitter_update', 1) call s:christmas_glitter_start() autocmd BufWipeout <buffer> call <SID>christmas_glitter_stop() endif command! -nargs=0 -bar -buffer ChristmasTreeTurnOn call <SID>christmas_glitter_start() command! -nargs=0 -bar -buffer ChristmasTreeTurnOff call <SID>christmas_glitter_stop()
詳しくは説明しませんが,timer_start()
を使って1秒毎に christmastreeGlitterSmall
と christmastreeGlitterLarge
のリンクするハイライトグループを書き換えることでハイライト色を変更しています.
JavaScript の setInterval
のようにタイマーは非同期に呼ばれるので,s:christmas_glitter_tick()
が実行されるわずかな時間しかユーザの操作をブロックしません.
なので,Vim の中にクリスマスツリーをピカピカ光らせながら普段のコーディングができます.
ftplugin/christmastree.vim
はファイルタイプ christmastree
がセットされるたびに毎回読まれるので,その中に直に処理を書けば良く, g:christmastree_glitter_update
が 0
にセットされていないときのみタイマーを開始するようにしています.
また,手動でタイマーを止めたり開始したりできるように,:ChristmasTreeTurnOn
と :ChristmasTreeTurnOff
も定義しています.-buffer
付きで定義しているので,そのバッファ内のみで使えるコマンドとして定義されます.
これでクリスマスの準備もバッチリですね!(?)
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 を回したい
ただし下記の点は気をつけておいたほうが良さそうです