おそらくはそれさえも平凡な日々

Dependabotを使ってGoプロジェクトの依存を更新するノウハウ

システムを運用していく以上、ライブラリは常に最新を使いたい。最近は依存ライブラリの更新を検知してくれる便利なサービスがいくつかあって、Nature社ではDependabotを使っている。

https://dependabot.com/

Renovateの方が便利そう、という話も聞くのだが、とりあえずDependabotはGitHubが買収して、privateリポジトリでも無料で使えるので利用している。

導入自体は簡単で、画面のガイドどおりに進んでいけば、良い感じに言語や依存管理ツールも自動検出してくれる。設定ファイルは特に置いていない。慣れてきたり、設定を横展開したくなった場合に置くと便利そう。

参考: Dependabotの設定ファイルを置くようにした

動作の様子

前提としてGo Modulesで依存管理をしているが、依存ライブラリの更新があると以下のようにpull requestを上げてくれる。

差分等も見やすく出してくれるし、まあまあ便利。pull request上にコメントしてbotを操作するのも面白いが、その手引も丁寧に書いてある。

しかし、いくつか使いづらい点があるので自前でハックしてなんとかしている。この辺りは諸々公式対応を望む部分。

go mod tidyして欲しい

これが一番大きな問題。以下のissueでやり取りされている。

https://github.com/dependabot/feedback/issues/215

dependabotのpull requestは、go get <module>@<new-version> しかしていない状態で上がってくる。つまり推移依存の解決は全くやってくれない。本来であれば、go mod tidyを実行して依存ツリーの構築をし直して欲しいところなのだがそれをやってくれない。

とは言え、実はgo mod tidyを実行するのはそう簡単ではない。公開ライブラリだけ使っていれば話は簡単だが、プライベートモジュールがある場合それらへのアクセス権がないとgo mod tidyが失敗してしまうからだ。

上のissueもだいぶロングランで苦戦している。去年の5月の以下のコメントがちょっと愉快。

We've been busy with the acquisition recently so haven't made progress on this yet. I'm hoping we'll be able to build out better Go support over the next few months though. -- https://github.com/dependabot/feedback/issues/215#issuecomment-496516828

自前でgo mod tidyして更新をpushする

どう対処しているかというと、dependabotのpull requestの更新をフックして、CircleCI上でpull requestの上書き更新をしている。具体的には、以下のようなシェル芸を.circleci/config.ymlに仕込んでいる。

command: |
  if $(echo {{CI{RCLE_BRANCH}" | grep '^dependabot/' >/dev/null) && [ "${CIRCLE_USERNAME}" = "" ]; then
    go mod tidy
    go mod vendor
    git add vendor go.sum go.mod
    export GIT_COMMITTER_NAME=nature-bot
    export GIT_AUTHOR_NAME=nature-bot
    export EMAIL='nature-bot@example.com'
    git commit -m "make vendor"
    git push origin "${CIRCLE_BRANCH}"
  fi

以前のエントリで説明したとおり、CircleCI上ではMachine account(この場合nature-bot)に、privateリポジトリへのアクセス権限を与えているので、その権限を使って、go mod tidy および go mod vendor (vendoringしているので) を実行して、その更新差分のcommitとpushをおこなっているという具合。pushできる権限渡してしまうのは嫌な感じがするが致し方ない。

dependabotによる更新かどうかを状況証拠的に判別している。上のif条件がそれだが以下の通り。

特殊なbotによる更新であるためか、$CIRCLE_USERNAMEが空文字列なのである。これはどこにもドキュメントはされていないので急に変わる可能性はある。

ちなみに、自前でpull requestブランチに更新をかけているので、Dependabotに対して一部追加の操作がさせられなくなる。例えば追加の依存が降ってきた場合にそれを更新させられないなど。その場合は @dependabot recreate とpull requestにコメントすることで、Dependabotにpull requestを作り直させている。

セマンティックバージョニングされていないライブラリは更新対象外となる

これも地味に困る点。タグが適宜打たれていないライブラリはDependabotは更新を抽出してくれない。また、ライブラリのセマンティックバージョン以外の地点(コミットハッシュ)に依存している場合も更新されない。

後者の場合は、単純にセマンティックバージョニングされた地点まで依存ライブラリを自力で更新すれば、以後はDependabotの更新対象となる。

問題は前者。基本的にはライブラリ作者に働きかけてバージョニングをしてもらうのが良いし、現在は多くが対応していますが、ポリシーとしてバージョニングをしていないライブラリ類が困りもの。

つまりはよく使う golang.org/x 系がバージョニングしないポリシーなので困るのです。

この辺りは推移依存で上がっていくのに任せるか、go get golang.org/x/net@latest みたいなコマンドを定期的に実行して手動で依存を更新する感じになります。ここもDependabotの公式対応が望まれます。

更新がうるさい場合

これは些末な問題だし、頑張って対応するのが本道。ただ、ライブラリによっては関係ない依存更新がバンバン飛んできてめんどくさいことがあります。まあ、aws-sdk-go のことなんですけど。

その場合の対処方法の一つとして、 @dependabot ignore this minor version というコメント操作があります。これは「このマイナーバージョンの間は更新を無視する」という意味で、次にマイナーバージョンが更新されるまではDependabotによる当該ライブラリに対するpull request起票がなくなります。

まあ、aws-sdk-goのパッチアープデートはほぼノールックマージでもいいとは思うし、実際場合によってはチラ見でマージしています。

まとめ

不便な点もあるが、なかった時代よりかは格段に便利なので助かってはいる。変なハック無しで公式で全部まかなえるようになって欲しい。

ただあまりアップデートが活発ではないような…?というところが気になっている。

smartcache ~ プリフェッチするインメモリキャッシュ

https://github.com/Songmu/smartcache

smartcacheというGoのインメモリキャッシュライブラリを書いた。

一般的に、キャッシュを実装する場合以下のような問題が起こりがちです。

smartcacheは上記の問題を以下のアプローチで解決しています。

使い方

コンストラクタにキャッシュの実際の有効期限、softな有効期限、そしてキャッシュ生成関数を渡します。

// コンストラクタ
ca := smartcache.New(
    10*time.Minute, // 実際のexpire
    time.Minute,    // soft expire
    func(ctx context.Context) (interface{}, error) {
        var val = 1
        return val, nil
    })
// キャッシュ取得
val, err := ca.Get(context.Background())

キャッシュ取得時に、soft expireを過ぎていた場合には既存のキャッシュを返しつつ、内部的に非同期でキャッシュの更新処理をおこないます。

このライブラリの特性上、あまり起きてほしくありませんが、実際の有効期限も切れていた場合には同期的にキャッシュの更新をおこない、その値を返します。これは一般的なキャッシュライブラリ同様の挙動です。soft expireに0を指定した場合は常にこの挙動となりますが、この挙動を利用することは少ないでしょう。

細かい挙動の話をすると、New時点ではキャッシュの生成はおこなわれず、初回のGet時におこなわれます。なので、キャッシュ生成に時間がかかるものを扱う場合、事前に一回Getを叩いておいてもよいでしょう。

このように、smartcacheはほぼフレッシュな値を即時に手に入れたい場合に重宝するはずです。ぜひご利用ください。

余談

こういう処理をちゃんと書くのは地味にめんどいしバグりやすいのでライブラリとしてまとめました。実際最初に書いたやつはテスト書いたらバグっていた…。既存のライブラリがありそうに思うのですが、上手く見つけられなかったので書いてみました。より良いやつがあれば教えて下さい。

このライブラリはKVSのようにキー文字列を指定するような作りにはしていません。不特定な値よりかは、ある程度温め続けて欲しい特定の値を扱うため、失効処理などは一旦必要ないと考えたからです。ただ、なにか良い感じのインターフェースがあればご提案くださると嬉しいです。

汎用キャッシュライブラリなので値が interface{} になってしまうのはちょっとダサい感じはある。この辺りは僕の設計力の問題もありますが、ジェネリクス的なものが欲しくなってしまう部分ではありますね。

ということで、結構特定用途に寄ったライブラリなので、smartcacheと言ってしまうのは名前負け感がちょっとあるのですが、その辺りはあまり良い名前が思いつかなかった…。

ecs-deployからecspressoに乗り換えた

のがもはや半年前だけど記録として書いておく。結論を書くと、ecs-deployからecspressoに乗り換えるのはすぐできるし、タスク定義が管理しやすくなるのでおすすめです。

https://github.com/kayac/ecspresso

もともとNature社では僕が入社する前からecs-deployが使われていた。これは、コンテナイメージをすげ替えてdeployするだけであればシンプルでわかりやすい。ただ、以下のような課題があった。

それに対して、ecspressoは以下のような利点があった。

タスク定義をリポジトリ管理できるようになったのが一番大きなメリットで、サイドカーの追加やパラメーターの変更などがpull request上でレビューできるようになった。

deployのフロー

社内のdeployはCircleCIから実行している。masterブランチが進むと勝手にdeployされるようになっている。これをecs-deployからecspressoを使うようにした。ecspressoのバイナリはかっこ悪いがリポジトリに突っ込んでいる。今だとOrbがあるのでそれを使うのが良さそう。

https://circleci.com/orbs/registry/orb/fujiwara/ecspresso

ちなみに、ecspressoのデフォルト挙動ではサービスが更新されてdeployの完了を待つ挙動になっているが、我々のユースケースだと、場合によってはいくつかのサービスを同時にdeployする関係上、そこでブロックして欲しくなかった。

なので、サービス更新がかかったらすぐに抜ける --no-wait オプションを追加するpull requestを送って取り込んでもらった。こっちのほうがecs-deployの挙動にも近いので乗り換える場合はおすすめです。ちなみに、--no-wait で一旦抜けた際にサービス更新完了を待ち受けたい場合は ecspresso wait コマンドを実行することでサービスの更新完了まで待機することができます。

以前git-pr-releaseのエントリに書いたように、Nature社では、featureブランチがdevelopブランチにマージされたら、developからmasterへのpull requestが自動で作られるようになっており、そのマージボタンを押したらさながらワンクリップデプロイの要領で本番に反映されるので体験が良い。

設定ファイル類の配置や課題など

設定類は今の所リポジトリの conf/ecspresso/ 配下に諸々配置している。例えば以下のような具合。

$ tree conf/ecspresso
conf/ecspresso
├── production-api.yaml
├── production-worker.yaml
├── production-consul.yaml
├── production-datadog-agent.yaml
├── production-nginx.yaml
├── taskdef-api.json
├── taskdef-worker.json
├── taskdef-consul.json
├── taskdef-datadog-agent.json
└── taskdef-nginx.json

YAMLが設定ファイルで{env}-{service}.yaml、JSONがタスク定義でtaskdef-{family}.jsonという命名規則にしている。サービス定義は配置していないが、管理したくなったら配置するかも知れない。

YAMLの設定ファイルがアプリケーション及びデプロイの単位となるが、一つのリポジトリの中に複数のアプリケーションがあるのはTwelve-Factor App的には良いことではないが、サブコンポーネント的なちょっとしたアプリケーションのためだけにリポジトリを分けるのも大げさなのでこのように管理している。

上記のAPIとWorkerについてはmasterブランチが進んだら愚直に両方deployされるようにしていて、その他のサブコンポーネントの類は一部手元からecspressoコマンドを実行する形でdeployしている。

手元のコマンド実行でdeployするのはあまり良いことではないので、masterブランチが進んだ時に更新が必要なdeployを洗い出して自動的に出ていくようにするのが良いと考えていて、そのあたりはBazel的なやつを入れるのが良いのかなーとか考えたりしている。ということでモノレポ指向になっていくかもしれない。

Goでテスト中に現在時刻を差し替えたりするflextimeというのを作った

https://github.com/Songmu/flextime

flextimeはテストコードの中で現在時刻を切り替えるためのライブラリです。Sleep時に実際に時間を止めずに時間が経過したように見せかける機能もあります。

つまり、PerlのTest::MockTimeやRubyのtimecop的なことをしたいわけですが、Goだとグローバルに関数の挙動を切り替えるといったことはできないため、利用にあたってはtimeパッケージで使っている関数を、flextimeパッケージに切り替える必要があります。

具体的には、flextimeはtimeパッケージと同様のインターフェースを備える以下の9つの関数を提供しています。

now := flextime.Now()
flextime.Sleep()
d := flextime.Until(date)
d := flextime.Since(date)
<-flextime.After(5*time.Second)
flextime.AfterFunc(5*time.Second, func() { fmt.Println("Done") })
timer := flextime.NewTimer(10*time.Second)
ticker := flextime.NewTicker(10*time.Second)
ch := flextime.Tick(3*time.Second)

これらはデフォルトでは標準timeパッケージと同様の動作をするため、単純に置き換え可能です。ちょっと乱暴ですが、以下のような単純置換でも大体動くでしょう。本来静的解析でやるべきですが…。

go get github.com/Songmu/flextime
find . -name '*.go' | xargs perl -i -pe 's/\btime\.((?:N(?:ewTi(?:ck|m)er|ow)|After(?:Func)?|Sleep|Until|Tick))/flextime.$1/g'
goimport -w .

使い方

SetFix関数を使うことで上記の9つの関数の挙動を差し替えます。Restoreでもとに戻します。

heisei := time.Date(1989, time.January, 8, 0, 0, 0, 0, time.Local)
flextime.Set(time.Date(heisei) // 時刻をセットする
now := flextime.Now() // 現在時刻がセットした時間(heisei)になる
// ...
flextime.Restore() // 元の挙動に戻す

SetFixの違いは、Setは実際の時間経過の影響を受けますが、Fixは完全に固定されるところです。

仕組み

内部的に時刻を返すオブジェクト(内部clock)を差し替えることで実現しています。それは、上の9つの関数を備えるClock interfaceを満たしたオブジェクトです。flextime.Nowなどの関数はそのオブジェクトに処理を委譲している形になります。

type Clock interface {
    Now() time.Time
    Sleep(d time.Duration)
    Since(t time.Time) time.Duration
    Until(t time.Time) time.Duration
    After(d time.Duration) <-chan time.Time
    AfterFunc(d time.Duration, f func()) *Timer
    NewTimer(d time.Duration) *Timer
    NewTicker(d time.Duration) *Ticker
    Tick(d time.Duration) <-chan time.Time
}

このClock interafaceというアプローチは以下の様な先行実装を参考にしました。

そのinterfaceをより標準timeパッケージに近づけつつ、内部的に差し替えるアプローチにより、easyに使えるようにしたのが、このflextimeです。

おまけ

組み込みのSet, Fixだけではなく、独自のClockインターフェースを備えたオブジェクトを作って、それに内部clockを差し替えることもできます。

flextime.Switch(clock)

また、Clock インターフェースの関数群は、NowSleepの2つさえあれば、他の関数を実現することができます。これを NowSleeper インターフェースとして定義しており、これを満たすだけで、以下のように独自Clockを簡単に作ることができます。

var ns flextime.NowSleeper
var clock Clock = flextime.NewFakeClock(ns)
flextime.Swtich(clock)

なかなかflexibleと言えるのではないでしょうか。名前のflextimeも気に入っています。

testabilityの向上に有用ではないでしょうか。ぜひ使ってみてください。

ちなみに、弊社はフレックスタイム制を採用しています。採用にご興味があればぜひご応募ください。

https://nature.global/jp/careers

二十四節気スプリントシステムのススメ

tl;dr

二十四節気スプリントシステムとは

今年から、Nature社でもスクラムっぽくスプリントを回し始めたのですが、前職のチームでも採用していた二十四節気スプリントシステムを導入しました。

二十四節気スプリントシステムというと大げさですが、これは単に、各2週間スプリントの名前に二十四節気を割り当てるものです。二十四節気のWikipediaのリンクを以下に載せておきますが、最近はGoogle検索の結果にも出てくるので驚きです。

二十四節気 - Wikipedia

小寒

ちなみに、これは、Mackerelチームでスクラムを採用し始めた頃に、当時のスクラムマスターであったid:motemenが発案したものです。

参考: はてなMackerelチームの開発フロー(スクラム、リモート)について話しました

これが地味に便利なのでこのエントリをしたためています。

二十四節気スプリントシステムのメリット

スプリントに適切な名前がついていると、会話のときに「〇月〇日から〇月〇日までのスプリント」とか言わずに済みますが、その名前がほぼ自動で決まるので便利です。例えば、「次の大寒スプリントでは云々」みたいな話ができる。

ちなみに今は小寒スプリントです。

季節感があるのも良くて、振り返りのときとかに「次は立春スプリントです!」とか言うと「オオッ」っと何故か妙に盛り上がったりします。ちなみに、立春スプリントは2月の頭から始まります。旧暦ベースなので季節の移り変わりが一足先に感じられるのも、若干前倒しの意識で事を進められる雰囲気が出るので地味に気に入っているポイントです。

あまり聞き馴染みのない節気のほうが逆に記憶に残るというのもあり、例えば芒種は、Mackerelチームが2014年に実際にスプリントを回し始めた最初の節気だったらしく、それが当初メンバーの記憶に残っており、節気が一周したときの振り返りで「そう言えば芒種って聞き覚えがある」みたいな話が出て、スクラム1周年に気づくことができてめでたい気持ちになった、というのがあったりしました。

ズレへの対処

二十四節気は一年を24分割するわけですが、これを2週間で単純に回すと48週間なので、一年間で4週間ずれてしまいます。適宜調整すればよいのですが、その辺りも年末年始とゴールデンウィークとシルバーウィーク辺りで結構吸収できます。

地味なポイントとしては「年始は小寒で始める」ようには調整したほうが良くて、そうすれば「2020小寒スプリント」のように、年をまたいでもスプリント名のユニーク性をを担保できます。

まとめ

ということで、二十四節気スプリントシステムの紹介でした。2週間スプリントを敷いている場合、ぜひ導入してみてください。

弊社は今後外国人が開発チームに加わる可能性もありますが、その場合でも二十四節気の趣を感じて貰えればよいかと思っています。このように開発フローの改善にも取り組んでいる昨今ですが、エンジニアを絶賛募集中ですので、ご興味ありましたら採用にご応募ください!

https://nature.global/jp/careers

ghq v1リリースとghq-handbookのお知らせ

https://github.com/motemen/ghq/releases/tag/v1.0.0

年末にアナウンスしていた通り、先程ghq v1.0.0をリリースしました。変更点は以下のエントリでお知らせしていたとおりです。その他Subversion周りの対応を無駄に頑張って強化したりしました。

https://songmu.jp/riji/entry/2019-12-28-ghq.html

是非ご利用ください。

ghq-handbookのお知らせ

年末年始休暇中にドキュメントを書いていたのですが、思ったよりもしっかりとした分量になったので、思い立って電子書籍にして販売してみることにしました。

https://leanpub.com/ghq-handbook

日本語で20ページほどです。値段は$1.99くらいにしたかったのですが、Leanpubで収益を上げる場合には$4.99が下限のようなので、その額に設定させてもらいました。

この本は、ソースのMarkdownを以下のリポジトリで公開しているため、無料で読むことも可能です。pull requestももちろん歓迎です。

https://github.com/Songmu/ghq-handbook

ghqの使い方を網羅した必読の一冊となっております。「こう使ってほしい」が書かれているので、ghqユーザー全員に読んでほしいと思っています。

現状、Leanpubの無料プランを使っており、デザインなども簡素です。この辺りも収益を元にブラッシュアップしたいと思っています。具体的には以下のようなものに投資したい。是非購入をご検討ください。

もちろん、この辺りの項目について協力してくださる方も大歓迎です。

今後ともghqを便利にご利用ください!

2019 -> 2020

2019年は転職がやはり一番大きな転機。Nature Remo Eをなんとか年内にリリースできたのでかろうじて及第点というところ。

Nature Remo Eはこれから電力事業に舵を切る上で非常に大事で、野心的な商品です。会社として非常に面白いフェーズなので興味があれば採用にご応募ください。

https://nature.global/jp/careers

去年は一瞬で過ぎた。ライフイベント含めて色々あった。近年は、毎年去年より激動だな、と思っているのでこれは今年もきっとそうなるのだろう。

個人事業主やISUCONの話はBlogに書けてない。追って書くかもしれない。

何かを成し遂げたい

これだけ書くと具体性がなくすごくバカっぽい。いや、僕がこういう「ビッグになりたい」みたいなノリを馬鹿にしていたし、今でもどこかで馬鹿にしているからそう感じるのだろう。

去年末に、Findyに寄稿させてもらい、それを書く中で、改めて自分がこれまで何も成し遂げてないことに気がついた。

https://engineer-lab.findy-code.io/neet-to-cto

回り道をした結果、今更そういうことを思うようになった。そういう事が多い人生である。それもまた良しだとも思っているのだが。

これから2年を目処に何かを成し遂げようと思う。今の会社Natureがそのステージになる。Natureをどうにかしないといけない。Natureは順調だし、だからこそしくじることはできない。別に僕の力がなくても成功するだろう。だからこそだ。

Mackerelがそのステージになるのかと思っていた。それなりにやった感じもあるが、道半ば感は否めず「成し遂げた」と感じられるレベルには至らなかった。

中途半端な人生を送ってきたことが自分のしこりになっているのだ、ということにも気がついた。

小学校のときに日能研の全国模試で2位になったことが2回あるが、トップにはなれずじまいだった。その時は、ろくに勉強もしないでその成績が出せる自分に満足していた。ただ、あのとき本気を出していれば、トップを取れたんじゃないかと今からすると思う。結局あの時が一種のピークで、その後何かのトップになれたことも無い。

その後の人生でそれなりに真面目に取り組んできたことが、全てそこそこ止まり。チェスもボウリングも自転車ロードレースもサイキックフォース2012も中国語もプログラミングも。一流になりきれない。

それで良いと思ってそれなりに満足して生きていたんだけど、ここに来て欲が出てきた。半端者でも良いから、今の会社で全方位に何でもやっていこうと思う。開発以外にもやらないといけないこと、やりたいこともたくさんある。楽しみである。

やることが多くて迷っている時間が多いとも自覚しているので、そこは改善していきたいし、もっとやれるはずだとも常々思っている。

経営者としてレベル1のペーペーだというところが大きな課題なので、そこは精進していく。勉強したり、他者の経営者とお話させてもらったりというところを増やしていきたい。

ghqで仕事用と趣味用でディレクトリ分けしてリポジトリ管理しやすくなりました

2020年1月5日追記: v1リリースしました https://songmu.jp/riji/entry/2020-01-05-ghq-v1.html

https://github.com/motemen/ghq

最近のghqの状況と、v1リリースに向けた非互換変更などのアナウンスです。現状の最新のv0.17.4を前提に書きます。

ghq.<url>.root 設定により細やかにclone先を設定可能に

例えば、gitconfig上に以下のように設定すれば、デフォルトでは"~/src/hobby" に、仕事用は"~/src/work"にcloneされます。

[ghq]
root = ~/src/hobby

[ghq "https://github.com/myorg"]
root = ~/src/work

[ghq "https://myvcs.example.com"]
root = ~/src/work
vcs = hg

このghq.<url>.rootの機能と併せて、includeIfでディレクトリ配下固有のgitconfigを読み込ませると有用でしょう。

[includeIf "gitdir:~/src/work"]
path = ~/src/work/.gitconfig

ghqはopinionatedなディレクトリ構成を強制するツールであるため、あまり柔軟なghq.root設定をできるようにしたくありませんでしたが、これは良い落とし所であると思っています。

ghq.rootを複数設定している場合の非互換変更

この変更によりghq.rootを複数設定している場合に意図しない非互換変更が入ったのですが、すみませんが、これを仕様とさせてもらいます。

具体的には、設定ファイル上に複数の、ghq.rootが書かれている場合 後に書かれている方が優先 されるようになりました。

具体的には、以下のようなgitconfigがあった場合、v0.17.0以降で、ghq getは"~/proj_b"にcloneをおこないます。

[ghq]
root = ~/proj_a
root = ~/proj_b

後に書かれたものが先に書かれた設定を上書きするのは、gitconfig上では自然な挙動なので、それを正しい仕様とさせてもらいます。

複数設定されている皆様はお手数ですが、設定の変更をお願いします。

ghq listの高速化

多くの貢献により、ghq list がだいぶ高速化されました。ありがとうございます。特に v0.9.0~v0.12.6の間はだいぶ遅いため、アップデートをおすすめします。

また、 ghq list --vcs=git として、探索とリストアップをgitリポジトリのみに絞り込むことで若干速くなるハック(v0.13.1以降)がありますが、現状(v0.14.2以降)ではvcsオプションを付けなくても、速度差はほとんどありません。

ghq createによるリポジトリ新規作成

ghq create myproj とすることで、リポジトリを新規作成するようになりました。この場合、僕の環境だと $(ghq root)/github.com/Songmu/myproj にリポジトリが作られます。内部的にはディレクトリ作成と git init が実行されます。

コマンド実行完了時に標準出力に作成されたディレクトリパスが出力されるため、それを利用してその後の操作もおこなえます。

ghq create <repository URL>, ghq create myorg/newproj の様な指定ももちろん可能です。引数への指定ルールは ghq get <target> のtargetと同様です。

ghq.<url>.rootghq.<url>.vcs の設定も考慮するので、それらが設定されていた場合、適切なディレクトリパス配下で適切なVCSを使って初期化処理がおこなわれます。

現状の機能はそれだけですが、リポジトリ作成後になんらかの初期化コマンド実行(例えばgodzil newなど)をしたいニーズもあると思うので、そのあたりは何らかの連携方法を考え中です。例えば、引数で追加実行コマンドを与えられるなど。

ghq v1に向けて

オリジナル作者のmotemenさんとも話しましたが、issueも大体捌けて、欲しい機能も大体揃い、使い方も固まってきたため、近いうちにv1をリリースしようと計画しています。

それに伴い、幾つかの非互換変更を入れる予定です。すでにpull requestは作成済みです。これらに関して意見があればぜひお知らせください。

ghq.rootのデフォルト値を"~/ghq"に

https://github.com/motemen/ghq/pull/237

gitconfigにghq.rootが設定されていない場合、ghqは現状"~/.ghq"配下にリポジトリをcloneしますが、隠しディレクトリである必要は無いため、標準を "~/ghq" にとする予定です。

"~/src" 派も多いようで、そちらにするか迷ったのですが、"~/src" は一般的な命名であり、ghq以外の何かとコンフリクトする可能性も考えて、デフォルトにはしませんでした。そのあたりはghq.rootを適切に設定してください。

ghq lookの廃止

https://github.com/motemen/ghq/pull/243

ghq lookcpanm --look インスパイアで作られたサブコマンドですが、今は、ghq get --look で代用できますし、そちらのほうがcpanmの挙動とも近いと言えます。

これを廃止する理由は、意図せぬ利用がされていて、それに伴うハマりどころも多いからです。

ghq look はあくまでも「一時的なソースコードのチラ見」のための機能なのですが、これを作業ディレクトリの移動に使い、それゆえに意図せぬ挙動が起こってハマっているケースが散見されます。これはもちろん利用者側の問題ではなく、アナウンス不足であり、そのように誤解されやすい機能であるから故に、削除したいという意図です。

ghq look は子プロセスで別のシェルプロセスを起動して、ディレクトリを移動したように見せかけています。どうしてもそのようにするしかない。

しかしこうすると、シェルの設定がうまく引き継がれなかったり、元のディレクトリに戻るときにexitする必要があるといった直感的でない挙動があります。また、シェルを起動し直すため、シェルの起動時間がかかってしまうという問題もあります。

なので、作業ディレクトリの移動には以下のように、素直にcdをするのがおすすめです。

% cd $(ghq list --full-path --exact motemen/ghq)

また、多くの人がすでにそうしていると思いますが、pecoやfzfのようなフィルタツールと連携して移動する設定をしておくのが王道だとも思います。

ghq importの廃止とghq getへの機能統合

https://github.com/motemen/ghq/pull/244

表題のとおりです。ghq getghq importは機能が類似しており、ソースコード上も重複が多かったため機能統合することにしました。今までimportサブコマンドを利用していた部分をgetに書き換えるだけで動きます。以下のような具合です。

% cat repolist.txt | ghq get --parallel

ちなみに、v0.11.0で追加した並列取得のための--parallelオプションが便利です。最大6並列でrepositoryを取得します。

ドキュメント強化

古い情報も散見されるため、改めてまとまったドキュメントを整備したいと考えています。チュートリアルやレシピ集的なものがあるとよいかと思っています。以下のような選択肢を考えていますが、何が良いか迷っているので、ご意見いただけると嬉しいです。

ロゴとか欲しい…?

ご意見やフィードバックをお待ちしております

ということで、ご意見やフィードバックをお待ちしております。オンラインサロン「MOTEMEN」 の#goチャンネルでghqの開発について投下していることもあるので興味があればご参加ください。(って僕が宣伝しても良いのかな…?)

また、オリジナル作者のmotemenや僕はGitHub Sponsorも開設しているため、検討してもらえると嬉しいです。

GitHub Actionsを使ってGoプロジェクトのCI/CD及びカバレッジ計測をおこなう

GitHub Actionsを遅まきながら使ってみて、自分のアクティブなGitHub上のGoのOSSプロジェクトで知見がたまったので、共有するものである。

GitHub Actionsについて

非常に良い。VCSとCI/CDの統合は体験が良い。各種イベントをハンドリングできるが、そのイベントが元々Webhookで提供されていたものなので、Webhookを弄っていた身からすると非常に親しみやすかった。コードpush以外のイベントもハンドリングしてプログラマブルに扱えるので夢が広がる。

使い勝手とか具体的に良くなった点

リポジトリ直下の.github/workflows配下に既定のYAMLをpushすると、その設定にしたがって自動でアクションが動いてくれる。ブラウザ操作必要ないのは快適。

Goで扱う場合

オレオレGoオーサリングツールである、godzilに導入している。ちなみに、godzilで作られる雛形も、GitHub Actions前提となりました。詳しくはリポジトリをみて貰えばと思いますが、以下に解説を書きます。

テストの実施

.github/workflows/test.yaml の中身が以下。

name: test
on:
  push:
    branches:
    - "**"
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os:
        - ubuntu-latest
        - macOS-latest
        - windows-latest
    steps:
    - name: setup go
      uses: actions/setup-go@v1
      with:
        go-version: 1.x
    - name: checkout
      uses: actions/checkout@v1
      with:
        fetch-depth: 1
    - name: lint
      run: |
        GO111MODULE=off GOBIN=$(pwd)/bin go get golang.org/x/lint/golint
        bin/golint -set_exit_status ./...
      if: "matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'"
    - name: test
      run: go test -coverprofile coverage.out -covermode atomic ./...
    - name: Convert coverage to lcov
      uses: jandelgado/gcov2lcov-action@v1.0.0
      with:
        infile: coverage.out
        outfile: coverage.lcov
      if: "matrix.os == 'ubuntu-latest'"
    - name: Coveralls
      uses: coverallsapp/github-action@master
      with:
        github-token: ${{ secrets.github_token }}
        path-to-lcov: coverage.lcov
      if: "matrix.os == 'ubuntu-latest'"

ポイントは以下。

リリース

release.yaml が以下。

name: release
on:
  push:
    tags:
    - "v[0-9]+.[0-9]+.[0-9]+"
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
    - name: setup go
      uses: actions/setup-go@v1
      with:
        go-version: 1.x
    - name: checkout
      uses: actions/checkout@v1
      with:
        fetch-depth: 1
    - name: release
      env:
        GITHUB_TOKEN: ${{ secrets.github_token }}
      run: |
        export GOBIN=$(pwd)/bin
        PATH=$GOBIN:$PATH make crossbuild upload

pushイベントをsemverにマッチするtag限定でフックして、リリースイベントをキックしている。実プロセスとしては最後の行のmake crossbuild uploadが全てですが、内部的にはgoxzでcrossbuildして、ghrでアップロードしています。GITHUB_TOKEN環境変数をsecrets.github_tokenで受け渡せるのが嬉しいポイント。

これまで個人的にはtoken管理の煩雑さを嫌って、手元でビルドしてGitHub Releasesにあげるパターンが多かったのだけど、これで、GitHub Actions上でリリースするようにできたので嬉しい。今はアップロードにghrを使っているが、公式のアクションactions/upload-artifact の機能が充実してきたらそっちに乗り換えも考えたいと思っている。

ちなみに、on: create でcreateイベントを方法も検討したが、createイベントだとtagsで絞り込むことができないのでやめた。イベントごとに指定できるattributeが異なるのでドキュメントをよく読む必要があります。

バッジ

README.md等に貼り付ける用のバッジも当然あり、URL体系は以下の具合です。

https://github.com/<OWNER>/<REPOSITORY>/workflows/<WORKFLOW_NAME>/badge.svg?branch=master

godzilだと以下の通りです。

https://github.com/Songmu/godzil/workflows/test/badge.svg?branch=master

今後

actions/cacheの活用や、独自acitonの公開などをして知見を貯めて、社内環境への導入も伺いたい。

Go Modules時代に依存をghqで一括cloneする

プロジェクトルートで以下を実行するだけです。ghq import -Pで高速に並列cloneしているのがお洒落ポイントです。

go list -f '{{join .Deps "\n"}}' | xargs go list -f '{{if not .Standard}}{{.ImportPath}}{{end}}' | ghq import -P

以下のようにシェル関数を定義しても良いでしょう。

gomod-ghq() {
  go list -f '{{join .Deps "\n"}}' | xargs go list -f '{{if not .Standard}}{{.ImportPath}}{{end}}' | ghq import -P
}

Go 1.13もリリースされ、Go Modulesに対応したプロジェクトも増えてきました。GOPATH時代は $GOPATH/src に依存が自然とcloneされていたのに、そうではなくなったので、このようにghqで一括取得してやると便利です。

ghqは元々Goのパッケージ取得のルールを参考にして作られたツールなのに、逆にこのようにGoのライブラリを取得するためにghqが必要になったというのも面白い話ですね。

そういえば、みんなのGo言語の改訂2版出ていたので皆さん是非ご購入ください。改訂にあたっては、Goの依存管理周りをglideからGo Modulesにゴリっと書き換えました。改訂作業中にもGo Modulesの仕様調整がたびたび入るので結構大変でした。結局Go 1.13でGo Modulesは完全に正式化はされませんでしたしね。