くりにっき

フルスタックキュアエンジニアです

研鑽Rubyプログラミングを読んだ #研鑽Ruby

最近家で読書する習慣が薄れてたのですが *1 頑張って研鑽Rubyプログラミング(以下:研鑽Ruby)を読みました。

研鑽Rubyプログラミング ― 実践的なコードのための原則とトレードオフwww.lambdanote.com

雑感

リファクタリング:RubyエディションメタプログラミングRuby 第2版 をgemの開発やメンテナンス方面に特化させたような感じ。(2冊とも読んだのが結構前になるので厳密には違うかもしれない)

バージョンアップ時になるべく非互換を防ぐような機能開発を行ったりdeprecation warningを仕込んだりするのはgemに関係なく他の言語のライブラリ開発でも普通に参考になります。

僕は10年以上gemを開発してますが終始「わっかる~」って頷きながら読んでました。

そんな自分でもちょいちょい知らないこともあって紛れもなく中級者~上級者向けの1冊

読書中のメモ

  • ISUCONで使えそうなパフォーマンス改善ネタがちょいちょいあった
    • バックトレース無し例外とか
  • Array#firstArray#lastArray#[] に置き換えてパフォーマンス改善するやつはrubocop-performanceへのPRネタとしてもよさそう
  • sequelやsinatraなどの有名所のgemのDSLの実装方法が解説されていてむっちゃ参考になった
  • 今まで自分が何気なくやってきた作業の流れ(機能追加のPRを入れる前に、機能追加しやすくするためのリファクタリング系のPRを先にmainにマージする)に関して「系統的アプローチ」って名前がついてるの助かる
    • 名前がつくことで他の人に伝えやすくなる
  • パフォーマンス改善に関してトレードオフも提示してるのが好印象
  • Rubyでこんな書き方はしないだろーw」と思いつつも年1~2回くらい必要に迫られそうなので、また必要に応じて読み返しそう

宣伝

2023/5/11(木)~13(土)に開催予定の RubyKaigi 2023 に「Fix SQL N+1 queries with RuboCop」というタイトルで発表予定です。

rubykaigi.org

研鑽Rubyを読みながらちょいちょい「これ、RubyKaigiの発表資料に書いたやつだ~」「この発表資料をそのまま出すとPolished Rubyist *2からツッコミが入るから補足入れないと」ってのがあって焦りました。

実は今回の発表資料は2年前の社内勉強会の発表資料がベースになってるので研鑽Rubyの影響は受けてないです。 先に研鑽Rubyを読んでたらもっと違う形になってそう。

5/12(金)に僕のトークを聞きに来てくれる人は予習として研鑽Rubyを読んでるとそういった一致点を見つけられて面白いかもしれないです。

*1:主にマスターデュエルのせい

*2:研鑽Ruby読了済みのRubyist

GitHub Actionsで特定の条件の時だけenvironmentを設定したい

tl;dr;

environment三項演算子でいい感じにする

モチベーション

TerraformのワークフローをGitHub Actionsで動かしてるんだけど、terraform apply した時(具体的にはmainブランチか手動でのbuild実行時 *1 )のみ environment をセットしてDeployments *2に通知したかった。( terraform plan (Terraformのdry run)ではDeploymentsには通知したくない)

実装例

こんな感じ。

environment: ${{ ((github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch') && 'terraform-apply') || '' }}

github.com

分かりづらいんですが terraform apply 時のみDeploymentsに通知されています。

解説

environment に空文字を渡してもエラーにならず、非空文字が渡された時のみそれが environment としてDeploymentsに通知される」という仕様を利用しています。*3

実はGitHub Actionsに三項演算子は無いのですがそれっぽいものは実装することができます。

ググれば色々ありますが自分は下記を参考にしました。

qiita.com

*1:後者はトピックブランチでの手動applyを想定

*2:https://docs.github.com/ja/rest/deployments/deployments?apiVersion=2022-11-28

*3:将来的に挙動が変わる可能性はあります

rspec-parameterized v1.0.0をリリースした

リリースノート

https://github.com/tomykaira/rspec-parameterized/blob/master/CHANGELOG.md#v100-2022-12-31

主な変更点

rspec-parameterizedの実装を

の2つに分割しました。

普通にバージョンアップするだけで良ければ bundle update するだけでよくて、 RSpec::Parameterized::TableSyntax を利用していなければ Gemfile の記述を rspec-parameterized-core に差し替えることでnative extensionへの依存をなくすことができます。

詳しいことは https://github.com/tomykaira/rspec-parameterized/blob/master/UPGRADING.md を見てください

あとついでにRuby 2.6未満のサポートも完全に切りました。

bundle gem した時に spec.required_ruby_version = ">= 2.6.0" がついてきたのを消すかどうかちょっと迷ったんですが、メジャーバージョンアップする時くらいしか古いバージョンのサポートやめることができない *1 のでこの機会にRuby 2.6以上必須にしました。(Ruby 2.5のEOLは1年以上前なのでもうさすがにいいよね...)

コンテキスト

rspec-parameterizedのRuby 3.2対応を行った時に id:joker1007 から「TableSyntaxはオプショナルな機能なのでなるべく利用者にnative extensionをコンパイルさせたくない」というコメントがありました。*2

その後railsrspecみたいにgemを分割するのはどうか?という方針に関してPR上で合意を得られました。

Ruby 3.2 リリースパーティー presented by アンドパッド - connpass でjoker1007さんに会った時に直接話して、自分が作業することに関する合意を得られたのでgemを分割するようにしました。

頑張ったこと:可能な限り全てのコミットを移行した

素朴にやるならgemの中で使ってるclassやmoduleをそのままコピペするのが一番ラクなのですが、それだと新しいリポジトリで歴史も失われてしまいます。

そのためコミットもなるべく移行しました。

リポジトリからcherry-pickする方法は特に凝ったことはしておらず、 bundle gem で新しいgemのスケルトンを作った後に

git remote add v0-origin git@github.com:tomykaira/rspec-parameterized.git

で一時的にremoteを追加して必要なコミットを1つずつcherry-pickしています。

以前 id:koic がrubocop-railsやrubocop-performanceで似たようなことをやってた気がするのでその時の移行スクリプトやコマンドを見つけることができればよかったのですが、見つからなかったので必要なコミットを全て手でcherry-pickしてまわりました。

幸いなことにrspec-parameterizedは200コミットくらいしかなかったので気合でなんとかしました。(もう少しスマートなやり方があればよかったんですが別リポジトリにcherry-pickした時のコンフリクトの解消が結構多くて完全に自動化するのは難しそうだった...)

頑張ってcherry-pickしたおかげで新しく作ったリポジトリでもContributorsが引き継がれています。

それでは良いお年を!

*1:この辺はメンテナの好みによるんですが、僕は「古いバージョンのサポートをやめる = BREAKING CHANGE」と考えてる派なので古いバージョンのサポートをやめる時はだいたいメジャーバージョンを上げてます

*2:https://github.com/tomykaira/rspec-parameterized/pull/81#discussion_r1050584335

gem_rbs_collectionを使ってるgemをいい感じにCIする

前置き

gemを作る時にはだいたい最初に bundle gem コマンドでgemの雛形を作りますが、最近のbundlerだと sig/rbsファイルを格納するディレクトリ)とrbsファイルの雛形も作られるようになっています。

最近作った https://github.com/sue445/doorkeeper_jprbsをいい感じにCIするところまで持っていけたのでメモ。

やったこと

gem_rbs_collectionをsubmoduleとして追加する

https://github.com/ruby/gem_rbs_collection のREADMEに書かれているようにsubmoduleとして追加。

gemにsubmoduleを追加すると rake build した時に作られるgemファイルにも含まれないか心配だったのですが、調べたところsubmoduleの中身は含まれなかったのでgem_rbs_collectionを追加することで配布するgemファイルが肥大化することはなさそうです。

手元でrbs validateを叩けるようにする

Rakefile に下記のようなtaskを追加。 *1

desc "validate rbs"
task :rbs_validate do
  command = %w(
    rbs
    --repo vendor/rbs/gem_rbs_collection/gems/
    -r date
    -r forwardable
    -r uri
    -r faraday:2.5
    -r hashie:5.0
    -I sig/
    validate
    --silent
  ).join(" ")

  sh command
end

-r でrequireするものは作るgemによって変わりますが他の部分はだいたい使いまわしでよさそう。 -r を個別指定するのが面倒なんだけど今の所他にいい方法は見つからなかった...

CIでrbs validateを実行する

さっき追加した rake rbs_validateGitHub Actionsのworkflowで実行 *2

こんな感じ。

- run: bundle exec rake rbs_validate

submoduleとしてgem_rbs_collectionをdependabotで定期更新する

.github/dependabot.yml に下記のようなファイルを追加 *3 することでDependabotでsubmoduleも定期的に更新してくれるようになります。

version: 2
updates:
  - package-ecosystem: gitsubmodule
    directory: "/"
    schedule:
      interval: monthly

interval(daily or weekly or monthly)は適当なので適宜変えてください。

実際にDependanotでgem_rbs_collectionを更新した時のPRはこちら

github.com

DependabotがマージしたPRのジョブに対してsecretsを渡したい

前提

事象

Dependabotが作成したPRに対して @dependabot merge のようにコメントをつけるとPRのジョブが全て正常終了した時にDependabotが自動でマージしてくれるんですが、マージコミットに対するデプロイ用のジョブが実行される時にデプロイ時に必要なリポジトリのsecretsが取得できずにデプロイできなくて困りました。

GitHub Actionsの設定

      - name: Create Sentry release
        uses: getsentry/action-release@v1
        env:
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
          SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT_FRONTEND }}
        with:
          environment: production

ジョブの実行結果

確かにsecretsが渡されていない模様。(なにか値が渡されていれば *** のようにマスクされた状態でログに出るはず)

原因

GitHubのRepository secrets(リポジトリ全体で使えるsecrets)やEnvironment secrets(productionやdevelopmentなどの環境毎に使えるsecrets)*1リポジトリに対してWrite権限を持ってるユーザが実行したジョブにしか渡されないのですが、 @dependabot merge でマージを行った場合はリポジトリにWrite権限を持っていないDependabotがジョブを実行したことになってるためsecretsが渡されていませんでした。

解決策

Dependabot secrets に設定すればDependabotが実行したジョブに対してsecretsが渡されるようになります。

ただ、このDependabot secretsは本来Dependabotがプライベートなパッケージマネージャにアクセスするために利用してるものだと思うので、今回のようなケースはGitHubがあまり想定してない解決方法のような気がしています。(なので今後GitHubの仕様変更で使えなくなる可能性も十分ありうる)

2022/10/5 19:50追記

別の場所で

このユースケースならAuto mergeが良いかなと思いましたが、botにマージさせたい理由が何かあった感じでしょうか?

って質問があったので追記。

メンテしてるリポジトリが70〜80個あるんですが*2、起きている時間帯にdepandabotのPRがくるとqueueが詰まって業務や趣味開発に支障が出るので、自分が寝ている時間帯になるべくPRをマージまでさせたいというのが一番の理由です。 (このエントリだと説明端折っていたけど実際には https://github.com/marketplace/actions/dependabot-auto-merge@dependabot merge のコメントをつけています)

*1:https://docs.github.com/ja/actions/security-guides/encrypted-secrets

*2: Depebdabotを入れてるリポジトリだけでも20個以上はあると思う

個人gemのドキュメントを全部GitHub Pagesに移行した

自分が作ってるgemでは https://github.com/lsegal/yard でドキュメントを書くことが多くて、そのドキュメントをホスティングする場所として https://rubydoc.info/ を使っていました。

しかし https://rubydoc.info/ がここ最近ずっと重くて表示するのに十数秒待たされてイラッとすることが多かったので、一念発起して全部GitHub Pagesに移行しました。

3日間で多分30〜40リポジトリくらい作業したと思います。

作業内容はこんな感じ。

github.com

設定ファイルはほぼ全部コピペでいいんだけど、リポジトリ毎にGitHub Pageを有効化したり手元でyardを実行してwarningが出てないかの確認をしてたらそこそこ時間かかりました。

最近だと静的ファイルをgh-pagesブランチとかにコミットにしなくてもGitHub Actionsから直接GitHub Pagesにデプロイできるようになって便利。

github.blog

小ネタですがgemspecに下記のように書いておくと https://rubygems.org/ からリンクが貼られるようになります。

spec.metadata["documentation_uri"] = "https://sue445.github.io/rubicure/"

doorkeeper_jp gemを作った

doorkeeper_jpとは

DoorkeeperAPIクライアントgemです。

github.com

自分のアプリで Doorkeeper API を使おうと思ったんですがAPIクライアントがなくて作りました。

基本的にはいつもの感じのAPIクライアントなんですが、gemでdoorkeeperといえば超有名な https://github.com/doorkeeper-gem/doorkeeper がある関係でgemの名前やネームスペースをかぶらないようにするのが大変でした。(名前重要)