Pull Requestを自動で作成する
rnssh, rnzooの新しいバージョンを出そうとするときに、別リポジトリになっているhomebrew-rnssh, homebrew-rnzooを編集してpushするのが、地味に面倒だと思うようになり、他の自動化でも使えるかなと考えて、Pull Requestの自動作成をやってみるようにしてみました。
こんな感じで動かします。今回はversionとsha256 hashを渡してます。
bash genpr.sh 0.0.0 ab01cd23ef45ab01cd23ef45ab01cd23ef45ab01cd23ef45
コードはgistに。rnzooのhomebrewのバージョンアップ対応。
手順としてはすごく単純で、以下を行っているだけ。
実際にできたプルリク(テストなのでクローズしてます)
sedで編集している部分は、方法は何でもよく、コードで書き換えられるなら、他の用途にも使えそうです。AWSのCodenizeに使ってるroadworker(Route53)やmiam(IAM)などの定義追加や変更は、この応用でPull Requestを生成できそうな雰囲気。
hubコマンドは、githubをCLIで操作できるもので、githubのreleaseも作れるようなので、あとはそこをスクリプトにすれば、リリースが全部自動化できます。最後のhomebrewの反映だけプルリクをマージのワンクッション。*1
rnsshとrnzooのリリース自動化状況
参考
*1:今回の場合は別にPull Requestでなくてmasterにそのままpushするでもいいんですが、練習と個人的に最後の一押しは手でいいかなというところ。
golangのHTTPサーバを構成しているもの
golangのHTTPサーバは、少量のコードで動くものを作ることができます。内部的には、net/http
パッケージのServer
とHandler
で構成されます。
Server
は名前の通り、Serveするもので、HTTPをどのネットワークソースで提供するかを定義するものです。Handler
は、HTTPリクエストを実際にどう処理するかを抽象化したinterfaceで、ServeHTTP(w http.ResponseWriter req *http.Request)
を提供しています。
しかし、これらは簡易に使う場合、見えなくなっています。どうしてそうなっているかを理解することで、幾つかのテクニックを知ることができます。
コードが短くなる過程
よく例として出てくるHTTPサーバの実装例は以下のコードです。
通常版
package main import ( "net/http" ) func main() { http.HandleFunc("/index", func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("Hello world")) }) http.ListenAndServe(":3000", nil) }
GET /indexでHello worldを返します
curl http://localhost:3000/index Hello world
ここにはServerもHandlerも登場しません。これを省略せずに書くと、おおよそ以下のコードになります。
冗長版
package main import ( "net/http" ) func main() { m := http.NewServeMux() h := new(HelloHandler) m.Handle("/index", h) s := http.Server{ Addr: ":3000", Handler: m, } s.ListenAndServe() } type HelloHandler struct{} func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { w.Write([]byte("Hello world")) }
通常版と比べると長いですね。この冗長版を以下のような3つのテクニックで短く簡単な通常版にしていきます。
その1 構造体作ってServeHTTP()作る必要をなくす
冗長版では、HelloHandler
構造体を定義して、そこにHandler
interfaceであるServeHTTP(w http.ResponseWriter req *http.Request)
を実装しています。
機能を作るたびにServeHTTP()のための構造体を作るのは面倒です。そこで、ServeHTTP()
と同じ関数型のaliasであるHandlerFunc
が用意されています。
type HandlerFunc func(ResponseWriter, *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
HandlerFunc
型にServeHTTP()
を作り、内部的に自身の関数を呼び出しています。
これによって、2つのメリットが生まれています。
- わざわざ構造体を定義しなくても良くなった
- ServeHTTPという関数名に縛られなくなった
それをコードに適用すると
package main import ( "net/http" ) func main() { m := http.NewServeMux() // HandlerFunc型にキャストして渡す m.Handle("/index", http.HandlerFunc(index)) s := http.Server{ Addr: ":3000", Handler: m, } s.ListenAndServe() } // ただの関数に func index(w http.ResponseWriter, req *http.Request) { w.Write([]byte("Hello world")) }
func(http.ResponseWriter, *http.Request)
という関数を好きな名前で作り、HandlerFunc
にキャストするだけで良くなります。さらに、このキャスト自体も意識しなくても良いです。詳しくは次の節で。
その2 ServeMuxは使うので予め用意する
ServeMux
は、HTTPリクエストに対しての処理をPathごとに登録することができる構造体です。ServeMux
を使わなくても、HTTPリクエストを処理することはできますが、Pathごとの振り分けは余程のことがない限り必ず使うため、省略できるようになっています。
冗長版では、http.NewServeMux()
して、PathのパターンとHelloHandler
構造体を渡していました。これを短くするために、net/http
は予めパッケージの変数にDefaultServeMux
という変数を持っています。DefaultServeMux
に対する登録を行うことができる関数も用意されていて、そのうちの一つがhttp.HandleFunc(pattern string, func(http.ResponseWriter, *http.Request))
です。第一引数のpatternでPathを、そして第二引数はHandlerFunc
にキャスト可能な関数型です。前述のHandlerFunc
へのキャストを意識しなくて良い理由は、このhttp.HandleFunc()
が内部でキャストしてくれるからです。
これによって2つのメリットが生まれています。
- ServeMuxを自分でnewしなくて良くなった
- HandlerFuncにキャストせずに、Pathと決まった引数の関数を実装して渡すだけで良くなった
再びコードに適用します。
package main import ( "net/http" ) func main() { // ServeMuxは作らず、HandlerFuncへのキャストもしなくていい http.HandleFunc("/index", index) s := http.Server{ Addr: ":3000", // packageで用意されているDefaultServeMuxを使う Handler: http.DefaultServeMux, } s.ListenAndServe() } func index(w http.ResponseWriter, req *http.Request) { w.Write([]byte("Hello world")) }
また少し短くなりました。
その3 Serverも必ず使うので、1発に
Serverは、HTTPをどうServeするかを定義します。基本的にはどのネットワークソースでHTTPを提供するのかと、そのHTTPをどう処理するかのHandlerがあれば動きます。
冗長版では、Serverをnewしてport 3000番をAddrとHandlerを入れて、ListenAndServe()
しています。多くの場合、これだけで済むので、その2つだけで渡せば、内部的にServerを動かしてくれるhttp.ListenAndServe(adds string, handler Handler) error
が提供されています。
func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() }
それを適用(+無名関数化)すると、最初に書いた通常版のコードになりました。
package main import ( "net/http" ) func main() { // index()は無名関数に http.HandleFunc("/index", func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("Hello world")) }) // Serverは作らなくても、DefaultServeMuxで起動 http.ListenAndServe(":3000", nil) }
通常版では、http.ListenAndServe(adds string, handler Handler) error
の第一引数に、port番号を表す文字列を渡し、第二引数はnilです。nilを渡すとServerの内部で、前述のパッケージ変数のDefaultServeMuxが使われます。*1自分でHandlerを実装して、このListenAndServe()
の第二引数に入れてを呼ぶことも可能です。その場合でもServerの作成は省略できます。
ちなみに、Server
の作成を省略しないケースはというと、Server
には他にも各種Timeoutや最大リクエストサイズなど、HTTPを受け付ける上での制約を課すことができます。それを使う場合はServer
を自分でnewして、対象の変数に値を入れます。ここ参照*2
まとめ
以上で、冗長版が通常版に省略されていくテクニックを知ることができました。いずれも、ほとんどやることが定型化しているのであれば、できるだけ手間が少なくなるように設計されています。逆に、細かくやりたい場合は、省略せずに書くことで実現できます。
ライブラリを書いたりすると、このバランスが難しかったりしますが、net/httpパッケージで使われているやり方は参考になります。
参考
homebrewのformulaフォーマットをチェックする
ふとQiita見てたら、homebrewで配布するという記事に、auditでフォーマットチェックするといいよってコメントがついていました。 goでcliのコマンドを作ってhomebrewで使えるようにしてみた - Qiita
rnzooもrnsshも数年前にpecoを参考に作っただけで、チェックも知らなかったので実行してみました。
homebrew-rnzooのformulaファイルに試してみました。
brew audit --strict --online rnzoo.rb
まず、チェック用に、rubocop のインストールが行われました。ログは記録忘れ。brewで入るようで、ローカルのgem listには出てこないです。結果は、以下の通り。
rnzoo: * A `test do` test block should be added * `require "formula"` is now unnecessary * C: 5: col 12: Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping. * C: 10: col 12: Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping. * C: 14: col 17: Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping. * W: 19: col 5: Useless assignment to variable - `msg`. Error: 6 problems in 1 formula
- test doはテスト用のブロックを入れるということで、特にないのでとりあえずブロックを作るだけ。
require "formula"
はもういらないらしい(古いやつみたい)- Cの奴は、
\
使わないなら、"
使うようにということで"
に統一 - Wの奴は、無駄な変数
msg
に割り当ててるとのことで、変数は除去
test do適当に追加したら、caverts
はtest do
より前じゃないといけないとのこと。
* `caveats method` (line 26) should be put before `test block`
対応したコミットはこちら
セルフチェックできる仕組みいいですね。 homebrewの記事、新規に書いてアップデートしないとかなー。 reiki4040.hatenablog.com