くりにっき

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

yardで複数ディレクトリに同名ファイルがある場合にファイル一覧をいい感じにしたい

tl;dr;

ファイルに @title を埋め込むかyard用の中間ファイルを作る

コンテキスト

例えば下記のようなファイル構成があったとします

  • README.md
  • sub-dir/README.md

https://github.com/lsegal/yard でドキュメントを作るとファイル一覧で同じものが並ぶのでなんとかしたかったというのが発端

方法1: ファイルに @title を埋め込む

https://rubydoc.info/gems/yard/file/docs/GettingStarted.md#adding-meta-data-to-extra-files を参照。

これでもいいんだけどGitHubとかで見た時にyardのタグが見えるので見栄えが悪いのがちょっと難点。

方法2: yard用の中間ファイルを作る

.yard/sub-dir-README.md のような名前で sub-dir/README.md をincludeするような中間ファイルを作成。

{include:file:sub-dir/README.md}

.yardoptsなどで下記のように中間ファイルのパスを渡す。

.yard/sub-dir-README.md

こうすることで sub-dir/README.md にyardのタグを書かずにファイル一覧をいい感じにできます。

go-gem-wrapperでこの問題に困ってこっちの方法を採用しました。

github.com

マージした後で気付いたけど .yard/go_gem_README.md@title 書いてもよかったかも。

中間ファイルが増えるのもちょっと微妙だけど自分はこっちの方がまだマシかなという判断。異論は認める

自分がメンテしてるgemをほぼ全部GitHub Actionsからリリースできるようにした

きっかけ

https://github.com/smartbank-inc/action_ip_filter のソースを見てたら https://github.com/rubygems/release-gem が使われていたのでついカッとなってやりました。

3日間で計50リポジトリくらいで作業してgemをリリースしてまわったと思います。

作業全部終わってから気づいたけどCursorとかにやらせればよかったな...

参考にしたドキュメント

この辺はむっちゃ読みました。

github.com

guides.rubygems.org

guides.rubygems.org

参考にした設定

下記を参考にしました。

tag pushをトリガにしてgemをリリースするか workflow_dispatch を使って手動実行でgemをリリースするかは悩ましかったんですが、 rubygems/release-gem 内で bundle exec rake release を実行している *1ため後者の手動実行を採用しました。( rake release 内のgemのリリースフローでもtagを作ってpushするのだが、同名のtagが存在しているとエラーになって rake release が失敗するため)

実際にやったこと

下記のような作業を50個くらいのリポジトリでやってまわりました。

自分用の作業メモをそのままブログに載せてるので合わないものは適宜変えてください。

.github/workflows/release_gem.yml を追加

リポジトリから手動実行してgemをリリースしてGitHubにReleaseを作るワークフロー。

USER_NAMEREPO_NAMEGEM_NAME を適宜書き換える。

name: Publish gem to rubygems.org

on:
  workflow_dispatch:

jobs:
  release:
    if: github.repository == 'USER_NAME/REPO_NAME'
    runs-on: ubuntu-latest

    environment:
      name: rubygems.org
      url: https://rubygems.org/gems/GEM_NAME

    permissions:
      contents: write
      id-token: write

    steps:
      - name: Harden Runner
        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
        with:
          egress-policy: audit

      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

      - name: Set up Ruby
        uses: ruby/setup-ruby@eaecf785f6a34567a6d97f686bbb7bccc1ac1e5c # v1.237.0
        with:
          bundler-cache: true
          ruby-version: ruby

      - name: Publish to RubyGems
        uses: rubygems/release-gem@a25424ba2ba8b387abc8ef40807c2c85b96cbe32 # v1.1.1

      - name: Create GitHub release
        run: |
          tag_name="$(git describe --tags --abbrev=0)"
          gh release create "${tag_name}" --verify-tag --generate-notes
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

forkされたリポジトリでworkflowを実行できないようにするのを最初見た時 *2 は「なるほど〜」と思いました。

なのでリポジトリ名をハードコーディングしてた方がよさそう。(後述のrubygems.org側のTrusted publishersがあるのでどっちにしろgemリリース時にエラーになるんだけど)

あと、 gh release create でreleaseを作るので .github/release.yml *3を作っておくとリリースノートをいい感じにできそう。

自分のリポジトリで使ってる設定はこれ。 https://github.com/sue445/sashimi_tanpopo/blob/1f3566456f6f39198ed7468653bd52cd17660e8b/.github/release.yml

リポジトリにEnvironmentを追加

Environmentsから rubygems.org を作ります。

複数人でメンテするgemはRequired reviewersがあると、リポジトリへのコミット権は渡したいけどgemのpush権は渡したくような場合に制御できて安心ですね。

基本的に「Protected branch = デフォルトブランチ」になっていると思うので下記のいずれかを設定。

  • Protected branch only
  • デフォルトブランチ(mainブランチなど)を個別に設定する
    • Protected branchが複数あるけどgemのリリースはmainブランチだけで行いたい場合などはこっち

rubygems.orgでTrusted publishersを追加

https://guides.rubygems.org/trusted-publishing/adding-a-publisher/ を参考に設定。

下記を設定します。

  • Workflow Filename: release_gem.yml
  • Environment: rubygems.org

リポジトリ.github/workflows/releasepush を含むyamlがあると補完されてる*4ようなので、workflowのファイル名もそれに合わせておくとよさそう。

gemリリースの流れ

Before

  1. CHANGELOG.md を書く
  2. version.rb を編集
  3. ローカルで bundle exec rake release を実行

After

  1. CHANGELOG.md を書く
  2. version.rb を編集
  3. git push
  4. リポジトリからworkflowを手動実行

rubygems/release-gem を導入できなかったリポジトリ

https://github.com/ruby-go-gem/go-gem-wrapper ではrubygems/release-gemを導入できずに導入後にrevertしました。

リポジトリと同様の手順で導入したら、Gemfile.lockに書かれているgemのバージョンと違うバージョンをbundlerがロードしようとして謎でした...

詳しいログ。

bundler 2.7.2でのエラー

github.com

bundler 4.0.0でのエラー

github.com

2025/12/10 13:14 追記

id:r7kamura さんみたいにworkflow用のリポジトリを作ってそこに集約させるの全然アリだな。この方式パクらせてもらおうw

https://github.com/r7kamura/slimi/blob/08a68e419692a8d681e9e078a38fc40604cff6c1/.github/workflows/release.yml

github.com

2025/12/11 12:51 追記

現状だと無理っぽい...

Bundler v4.0.0でGo gemを作れるようにした

前置き

bundler v4.0.0で bundle gem --ext=go でGoのnative extension gem(以下、Go gem)のスケルトンが作れるようになりました。*1

blog.rubygems.org

[

--ext=go の実装を僕が作ったので宣伝させてください。

github.com

Before my PR(~bundler v2系)

bundle gem--ext はcとrustのみサポートされていました 。

Go gemを作る時は bundle gem <GEM_NAME> --ext=c でCのnative extensionのスケルトンを作った後で https://github.com/ruby-go-gem/go-gem-wrapper/tree/v0.10.0/_tools/patch_for_go_gem のようなスクリプトを実行してパッチをあてる必要がありました。

After my PR(bundler v4系~)

CやRustと同様に bundle gem <GEM_NAME> --ext=go でGo gemのスケルトンが作れるようになりました。やったね!

go-gem-wrapperについて

僕が作ったライブラリです。

github.com

主に下記の2つで構成されています

  • github.com/ruby-go-gem/go-gem-wrapper (Goのmodule)
    • GoからCRubyの関数を呼び出すためのbindingを提供している
  • https://rubygems.org/gems/go_gemRubyのgem)
    • native extensionのビルド時にGoをビルドするための処理を追加
    • go testgo fmt など、Goでよく使うコマンドをRubyから呼び出しやすくするためのrake taskを提供

bundle gem --ext=go した時に作られるスケルトンの中で github.com/ruby-go-gem/go-gem-wrapper やgo_gemが組み込まれています

Go gemやgo-gem-wrapperのもっと詳しい技術的な話について

RubyKaigi 2025 で話したのでそちらの発表資料やアーカイブをご参照ください。

speakerdeck.com

rubykaigi.org

inside.pixiv.blog

余談ですがRubyKaigiの時に喋ったPR *2 がマージされてbundlerに取り込まれたのでKaigi Effect *3 と言えます。

大変だった思い出

だいたいの苦労話はRubyKaigiで話してるので、今回はRubyKaigiで喋っていないbundlerのPRについて書きます

Windowsの時だけなぜか特定のテストがハングする

bundlerはGitHub ActionsのUbuntu, macOS, WindowsのRunnerでそれぞれCIを実行してるんですが、なぜかWindowsの時だけ特定のテストがハングするという現象があってハマりました。(未解決)

https://github.com/ruby/rubygems/pull/8183/commits/44fe3fedef6f8784dbb7e61d8a6cf869d6394d24

Windowsでのビルドが1時間くらいかかる(つらい)

寝る前にpushして、起きてからCIの結果を確認することが多かったです。

https://github.com/ruby/rubygems/actions/runs/19452908845/job/55661461150

【余談1】PRマージ直後の様子

なんやかんやでマージしてもらえるまで1年くらいかかったので喜びもひとしお。

トリアージしてくれたhsbtさんやPRをレビューしてくれた人たちに本当に感謝です...

【余談2】どうでもいい気付き

Bundler v4にGo(5)対応が入るの紛らわしいなw いっそBundler v5合わせでもっと寝かせておくべきだったか

YARDで継承関係を明示的に伝えたい

こないだgemを作ってて調べたやつのメモ。

前置き

例えば下記のように Hashie::Mash を継承した Pixela::Response があるとします

module Pixela
  require "hashie/mash"

  # Pixela API response
  #
  # @see https://www.rubydoc.info/gems/hashie/Hashie/Hash
  # @see https://www.rubydoc.info/gems/hashie/Hashie/Mash
  class Response < Hashie::Mash
    disable_warnings
  end
end

Hashie::Mash はさらに Hashie::HashHash を継承しているため *1、実際には「 Pixela::Response < Hashie::Mash < Hashie::Hash < Hash < Object」という継承関係になります。

これを YARD でリファレンスを生成した場合下記のように、「 Pixela::Response < Hashie::Mash < Object」という継承関係になってしまいます。

これはYARDが Hashie::Mash の実装を知らないためなんですが、3rd party製のgemの継承関係をどうやってYARDに伝えればいいか困ったので調べました。

解決方法:@!parse で書く

@!parse *2 を使うとYARDのコメントの中だけで有効なRubyのコードを書けるので、そこで外部のgemの最低限のクラス定義を書いてやれば表示されるようになります。

# @!parse
#   module Hashie
#     # @see https://www.rubydoc.info/gems/hashie/Hashie/Hash
#     class Hash < ::Hash
#     end
#
#     # @see https://www.rubydoc.info/gems/hashie/Hashie/Mash
#     class Mash < Hash
#     end
#   end

module Pixela
  require "hashie/mash"

  # Pixela API response
  #
  # @see https://www.rubydoc.info/gems/hashie/Hashie/Hash
  # @see https://www.rubydoc.info/gems/hashie/Hashie/Mash
  class Response < Hashie::Mash
    disable_warnings
  end
end

moduleの外側に書くのがポイントで、 moduleの内側に書くと class Pixela::Hashie::Mash のようなクラスのリファレンスが作られたので、 @!parse はスコープを解釈してるらしいです。

実際のパッチとYARDで生成されたリファレンスは下記

connpass API v2に対応したAPIクライアントgemを作った

前置き

先日connpass API v2が発表されました。

下記エントリでconnpass APIの個人利用申請をしてたため、先行してv2のAPI keyをもらっていました。

sue445.hatenablog.com

v1からv2に移行することで結構メリットがあるので自分のアプリでもv2対応したいんですが、そのためにはAPIクライアントを作る必要があるので作りました。

作ったもの:connpass_api_v2-ruby

github.com

開発期間は2日くらい。connpass API v2対応のAPIクライアントはおそらく世界最速なのではと思います。

https://connpass.com/about/api/v2/ に書かれているAPIは全部対応しています。

使い方

require "connpass_api_v2"

client = ConnpassApiV2.client(ENV["CONNPASS_API_KEY"])

client.get_events

res = @client.get_events(nickname: "sue445", count: 100)

res.events.count
#=> 100

res.events[0].title
#=> "STORES.rb RubyKaigi 2025 直前スペシャル"

res.events[0].url
#=> "https://hey.connpass.com/event/347445/"

詳しくはREADME参照

自作gemでモンキーパッチrbsを利用する

コンテキスト

僕が直近2〜3年以内に新しく作ったgemでは全てrbsとsteepを導入してガッツリ型を書いています。

しかし自分のgemのrbsは書けても、自分のgemが依存しているrubyの標準ライブラリのメソッドや依存している別のgemの方で型定義がなかったり足りていなかったりして steep check が通らなくて困ることがよくあります。

こういう時には https://github.com/ruby/gem_rbs_collectionhttps://github.com/ruby/rbs にパッチを送ればいいんですが、とはいえ本家に取り込まれるまで自分のgemの開発が止まるのはつらいのでfork版を使うことが多いでしょう。

しかし足りない型定義が多かった時にfork版をメンテするのもつらい気がしているので*1僕はモンキーパッチとしてrbsを入れるようにしています。

こういうことは多分自分しかやっていないだろうなあと思いつつ、他の人がどうやってるか知りたいので自分が普段やってることを紹介してみます。

構成

下記のように sig/non-gemify/ のようなディレクトリを作って、ここにモンキーパッチrbsを入れています。

$ tree sig/
sig/
├── non-gemify
│   ├── io.rbs
│   └── kernel.rbs
├── ruby_header_parser
│   ├── argument_definition.rbs
│   ├── config.rbs
│   ├── enum_definition.rbs
│   ├── function_definition.rbs
│   ├── parser.rbs
│   ├── struct_definition.rbs
│   ├── type_definition.rbs
│   ├── typeref_definition.rbs
│   └── util.rbs
└── ruby_header_parser.rbs

3 directories, 12 files

sig-non-gemify/ のようにしてもよさそうですが好みの問題かと思います。

エディタやIDEによっては sig/ 以外のディレクトリを認識しなくて困ることをなんとなくエスパーして sig/ の中にサブディレクトリを作る方法にしました。(杞憂かも)

手順

モンキーパッチrbsをgemファイルに含めて https://rubygems.org/ で配布を行うと、そのgemをインストールしたユーザの開発環境に悪影響が起きるかもしれません。

そのため、gemspecで下記のように除外設定を入れた方がいいです。

   spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
     ls.readlines("\x0", chomp: true).reject do |f|
       (f == gemspec) ||
-        f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
+        f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile sig/non-gemify])
     end
   end

あとはsig-non-gemify/ のように sig/ ディレクトリの外に置くようにした場合には下記のように Steepfile に設定追加も必要だと思います。

 target :lib do
   signature "sig"
+  signature "sig-non-gemify"

モンキーパッチrbsが完全になくなった時に Steepfile に存在しないディレクトリを書いた時にエラーにならないかも気になったんですが、今のsteepの最新版(1.8.3)だと問題なかったです。

モンキーパッチrbsを使うメリット

  • 本家のgem_rbs_collectionを併用できるので常に最新を使い続けることができる
  • 本家に投げたパッチが取り込まれた時に自分のgemではそのモンキーパッチrbsだけを消すだけでいい
  • 一気に取り込まれるとは限らないのでマージされたものから段階的にモンキーパッチrbsを消すことができる

最後に

みんなのgem開発時のこの手の困りごとの解決方法を知りたいので教えてください!!!

2024/12/05 20:27追記

ruby-jp slackで聞いたら

sig/gems/ ってディレクトリにいろいろ放り込んでますね。取り込まれた後にお掃除してます、

とのことだった。そうなりますよね...

*1:実際にそういう運用はしたことないのであくまで想像

地域.rbカレンダーでconnpass APIの個人利用申請をした

tl;dr;

connpass APIの仕様変更で 地域.rbカレンダー を閉じるつもりでしたが、個人利用申請を行ったので2024年5月23日(木)以降もカレンダーが更新されます

時系列

今年の2月にconnpass APIの無償提供終了が発表

3月頃にプランが発表されて、料金体系が個人向けじゃなさそうなので地域.rbカレンダーを閉じる(connpassの有料APIに対応しない)ことを決意

この時点では固定IPアドレスも費用かかりそうできっついなあということで閉じる方針は継続。

これを見てRubyKaigi後に機運が高まった。*1(実質KaigiEffect*2

申請した2日後くらいに許諾された!!!

詳しいログ

github.com

具体的にやったこと

2024年5月23日(木)以降もConnpass APIを使うには固定IPアドレスが必要なので、その対応を行いました。

地域.rbカレンダーのカレンダー生成のエンドポイントはCloud Functionsで動いているため*3Serverless VPC Access Connectorを利用してCloud Functionからインターネットに出る時にCloud NATを経由して送信元のIPアドレスを固定化しました。

変更前後の構成は下記のような感じです。

Before

After

この図だと端折っていますが、送信元のIPアドレスが本当に固定化されているかの動作確認としてVPC内にVMを立てたりもしています。(Cloud NATはVPCからインターネットに出る時の送信元を全て固定IPアドレスにするので、VPC内にVMを立てても当然同じIPアドレスになる)

Terraform

具体的な方針は下記が参考になりました。

dev.classmethod.jp

これを参考に実装したのが下記のTerraformになります。

# VPC
locals {
  subnet_cidr = "10.128.0.0/20" # 10.128.0.0 - 10.128.15.255

  # Requires /28 for Serverless VPC Access Connector
  # c.f. https://cloud.google.com/vpc/docs/serverless-vpc-access
  vpc_access_connector_cidr = "10.128.16.0/28" # 10.128.16.0 - 10.128.16.15
}

resource "google_compute_network" "main" {
  name                    = "main"
  auto_create_subnetworks = false
}

resource "google_compute_subnetwork" "main" {
  name          = google_compute_network.main.name
  network       = google_compute_network.main.self_link
  ip_cidr_range = local.subnet_cidr
  region        = "asia-northeast1"
}

resource "google_compute_router" "main" {
  name    = google_compute_network.main.name
  region  = google_compute_subnetwork.main.region
  network = google_compute_network.main.self_link
}

resource "google_vpc_access_connector" "main" {
  name          = "main"
  machine_type  = "f1-micro"
  ip_cidr_range = local.vpc_access_connector_cidr
  network       = google_compute_network.main.id

  # NOTE: requires 2+ instances
  min_instances = 2
  max_instances = 3
}

# Cloud NAT
resource "google_compute_router_nat" "main" {
  name   = google_compute_network.main.name
  router = google_compute_router.main.name
  region = google_compute_router.main.region

  nat_ip_allocate_option = "MANUAL_ONLY"
  nat_ips                = [google_compute_address.nat.self_link]

  source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"
}

resource "google_compute_address" "nat" {
  name        = "nat"
  region      = google_compute_subnetwork.main.region
  description = "Static Address for Cloud NAT"
}

アプリケーション側(デプロイ時にFunctionをVPC Connectorに紐づける設定)は下記

github.com

ハマったポイント:Terraformでapplyする時だけなぜかエラーになる

具体的には下記のようなエラーです

│ Error: Error waiting to create Connector: Error waiting for Creating Connector: Error code 13, message: An internal error occurred: VPC Access connector failed to get healthy. Please check GCE quotas, logs and org policies and recreate.
│ 
│   with google_vpc_access_connector.main,
│   on vpc.tf line 27, in resource "google_vpc_access_connector" "main":
│   27: resource "google_vpc_access_connector" "main" {

設定自体は問題なさそうだし、なんなら全く同じ設定をコンソールから設定できたので本当に謎でした。( terraform apply でのみエラーになる)

Terraformで利用しているサービスアカウントに対して service-PROJECT_NUMBER@gcp-sa-vpcaccess.iam.gserviceaccount.com の「サービスアカウントユーザー( roles/iam.serviceAccountUser )」*4 のIAMロールが必要なのが原因でした。 *5

最後に

作った後で気づいたけど自分の用途だとオーバーテクノロジーだったかもしれないです。

似たようなことをやりたい人は参考にして下さい。

*1:このポストはRubyKaigi直前のものなんだけど機運が高まったのはRubyKaigi後

*2:https://togetter.com/li/162817

*3:icsを返すエンドポイントを自分のGoogle Calendarに登録してからGitHub Pagesで公開している

*4:https://cloud.google.com/iam/docs/understanding-roles#iam.serviceAccountUser

*5:https://cloud.google.com/vpc/docs/configure-serverless-vpc-access?hl=ja#service_account_permissions