くりにっき

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

#fukuokark05 で不採択だったプロポーザルを公開します

tl;dr;

一句タイトルをどうしても見てほしかった

プロポーザルを書く前に考えたこと

下記は今回のプロポーザルの応募フォームより抜粋

福岡 Rubyist会議 05 のプロポーザル応募フォームです。プロポーザルは以下の要件を満たしてください。

自分にとっての「最近、何してる?」はOSS開発なのでそっち方面で出すことはうっすら考えていました。とはいえ具体的なネタは決まっていなかったです。

そんな折で先日参加したgoconで「日々困ってる問題は自分だけじゃないんだ」というエピソードがあったのでgemを作りました。

sue445.hatenablog.com

そして、プロポーザル内でも「こういうgemを作ってます」ってテーマで出しました。

実際に出したプロポーザルの内容

余談ですがプロポーザル書いてる時に「トーク概要」の100文字以内制限が一番大変だった記憶があります

タイトル

トークのタイトルを 30 文字以内で入力してください。これは公式サイトに記載されます。

gem作り 刺し身タンポポ 解決す

トーク概要

トークの概要を 100 文字以内で入力してください。これは公式サイトに記載されます。

私は多くのOSSをメンテしてます。リポジトリをたくさんメンテしてると各リポジトリに同じ変更を適用するのも刺し身タンポポ作業で大変です。今回はその問題を改善するために作ったgemの話をします。

トーク詳細

トークの詳細を 1,000 文字以内で記載してください(改行可)。これは外部に公開されません。審査員のみが参照します。

https://github.com/sue445/sashimi_tanpopo というgemを作った話をします。

アウトラインはこういうのを考えてますが変わるかもしれません。

https://sue445.hatenablog.com/entry/2025/10/26/121451 に書いたことがベースになりそうですがブログ公開からfukuokark05まで時間が経ってるので差分に関してもなんか話せると思います

アピールポイント

トークのアピールポイントを 1,000 文字以内で記載してください(改行可)。本カンファレンスで「なぜこのトークをしたいのか」「なぜあなたがこのトークをすべきなのか」を書いてください。特に Ruby 以外のトークをされる方は、その重要性や必要性を Ruby しか知らない審査員に理解できるようアピールしてください。これは外部に公開されません。審査員のみが参照します。

OSS活動をしてる人には大きく分けて「1つのOSSを深く狭くメンテするタイプ」と「たくさんのOSSを浅く広くメンテするタイプ」の2種類がいると思っています。

私は後者のタイプです。 浅く広くとはいいつつも自分の代表作の https://github.com/sue445/rubicure は10年以上コツコツメンテしてます。毎年新しいプリキュアが出るんで毎年必ず新しいバージョンを出してます!(熱弁)

前者の人の話はRubyKaigiなどでもよく聞きますが、後者の人の話はあまり聞かない気がします。

たくさんのOSSをメンテする人にはその人なりの問題があるので https://github.com/sue445/sashimi_tanpopo を作って解決しました。

たくさんのOSSをメンテしてる人にしか分からない苦労を共有しつつ、自分のようにたくさんのリポジトリをメンテしてる人やチームに解決策を持ち帰ってもらおうと思っています。

また、トーク詳細にも書いた https://github.com/itamae-kitchen/itamae は僕がメンテしてるOSSの1つなので、内部実装やメリデメを熟知してます。

Itamaeのメンテナ自ら「こういうケースではItamaeは向かない(だからこそ再実装する必要があった)」ってのは説得力があると思います。

結果

🥺

GitHub Actionsでsubmodule更新時のみjobを実行したい

背景

PRで特定のファイルが変更されたときだけjobを実行したいworkflowがあるとします。

name: "workflow for sub_dir"

on:
  push:
    paths:
      - "sub_dir/**/*"

同じリポジトリでこういうsubmoduleがあるとします。

$ cat .gitmodules 
[submodule "submodule_dir"]
        path = submodule_dir
        url = https://github.com/example/submodule_dir.git

.github/dependabot.yml でこういう風にsubmoduleの更新時にdependabotでPRを作っています。

version: 2
updates:
  - package-ecosystem: gitsubmodule
    directory: "/"

この時、submodule更新時のPRでうまくjobが実行されなくてちょっとハマったのでメモ。

解決方法

シンプルにこれでよかった。

name: "workflow for sub_dir"

on:
  push:
    paths:
      - "sub_dir/**/*"
      - "submodule_dir"

OSS開発の刺し身タンポポ作業を解消するためにsashimi_tanpopoを作った

sashimi_tanpopoについて

ファイルを特定のルールで編集して差分があった時にPull RequestやMerge Requestを作るためのgemです。

github.com

この例を見てもらうのが手っ取り早いと思います。

# recipe.rb

update_file ".ruby-version" do |content|
  content.gsub!(/^[\d.]+$/, params[:ruby_version])
end

update_file "Dockerfile" do |content|
  content.gsub!(/^FROM ruby:([\d.]+)$/, %Q{FROM ruby:#{params[:ruby_version]}})
end

@ruby_minor_version = params[:ruby_version].to_f

update_file ".rubocop.yml" do |content|
  content.gsub!(/TargetRubyVersion: ([\d.]+)/, "TargetRubyVersion: #{@ruby_minor_version}")
end

update_file ".github/workflows/*.yml" do |content|
  content.gsub!(/ruby-version: "(.+)"/, %Q{ruby-version: "#{params[:ruby_version]}"})
end
# Update local app files using recipe.rb
$ sashimi_tanpopo local --target-dir=/path/to/app --params=ruby_version:3.4.5 /path/to/recipe.rb

# Update local app files using recipe.rb and create Pull Request
$ sashimi_tanpopo github --target-dir=/path/to/app --params=ruby_version:3.4.5 \
--message="Upgrade to Ruby 3.4.5" --github-repository=yourname/yourrepo --pr-title="Upgrade to Ruby 3.4.5" \
--pr-source-branch=ruby_3.4.5 --pr-target-branch=main --pr-draft /path/to/recipe.rb

10月頭の3連休辺りから作り始めたので開発期間は3週間くらいだと思います。(gem以外のも部分も含む)

なぜ作ったか

1人で複数のOSSをメンテしてる場合、複数のリポジトリで一斉に同じ変更をしたいことが多々あります。

僕はこういう作業を「OSS開発の刺し身タンポポ作業」と呼んでます。

そのために https://github.com/sue445/myapp_version_upgrader というのを作ってました。

sue445.hatenablog.com

自分だけが使うなら別にいいんですが、OSSではあるものの特にライブラリ化とかはしていなかったため同じような問題に困ってる人がいる場合に勧めづらいのが難点でした。

そのため、gem化して他の人が使いやすくしました。

作る時に考えたこと

採用言語について

採用言語についてはいくつか考えました

  • Go
    • メリット:ビルド済みのシングルバイナリを配布できるので利用者側の実行環境を問わない、GitHubやGitLabの利用するためのAPIクライアントがそれぞれある
    • デメリット:Goの中で柔軟なDSLを作るのが大変
  • Ruby
    • メリット:柔軟な言語仕様で内部DSLを作りやすい、GitHubやGitLabの利用するためのAPIクライアントがそれぞれある
    • デメリット:利用者の実行環境に別途ランタイムとしてRubyが必要
  • mruby
    • メリット:ビルド済のシングルバイナリを配布できる、柔軟な言語仕様で内部DSLを作りやすい
    • デメリット:GitHubやGitLabの利用するためのAPIクライアントがmgem*1で見つからなかったので自作する必要がある

色々考えた結果Rubyで作ることにしました。

利用者側の実行環境に別途ランタイムとしてRubyが必要な問題に関してはgemがインストール済のDockerイメージを配布することにしました。

https://github.com/sue445/sashimi_tanpopo/pkgs/container/sashimi_tanpopo

Itamaeを使うかどうか

sashimi_tanpopoの前身となったmyapp_version_upgraderではDSLでファイルを編集する部分に https://github.com/itamae-kitchen/itamae を利用していました。ちなみに僕はItamaeのメンテナでもあります。

Itamaeは非常に便利なツールなのですが、今回のユースケースだと下記の問題がありました。

1. オーバースペック

Itamaeはインフラ構成を管理するためのツールでsshした先でpackageのインストールやserviceの有効化なども行ってくれます。

しかしsashimi_tanpopoではローカルのファイルを編集することができればいいので、Itamaeに含まれているインフラ操作に関する大多数の依存が不要でした。

そのため、Itamaeで実装されていたようなDSLを自分で実装しました。*2

DSLを自分で実装したことによるメリットもあったのでそれは後で書きます。

2. Itamaeは動的にパラメータを設定できない

Itamaeでパラメータを渡す場合には下記のようなnodeファイルを利用することになります。(この辺の仕様はChefやAnsibleも同様ですね)

# node.yml
ruby_version: "3.4"

実行時にファイル内にパラメータが静的に存在する必要があるので、実行時に動的にパラメータを渡したい場合にはちょっとした工夫が必要でした。

僕はmyapp_version_upgraderだと下記のように動的にnode.ymlを作っていました。

今回のユースケースだと実行時にパラメータを引数できるようにしたかったのでツール側でサポートするようにしました。

ちなみにnode.ymlでerb対応するようなパッチもあったのですが、Itamaeの方針にあわないという理由でリジェクトされたことがあります *3

Itamaeとの差分

glob対応

Itamaeで特定のディレクトリ配下の複数のファイルに対してレシピを適用したい場合、下記のような工夫が必要でした。

Dir.glob(".github/workflows/*.yml").each do |workflow_file|
  file workflow_file do
    action :edit

    block do |content|
      content.gsub!(/ruby-version: "(.+)"/, %Q{ruby-version: "#{node[:ruby_version]}"})
    end

    only_if "ls #{workflow_file}"
  end
end

しかし毎回 Dir.glob を手書きするのも大変なのでsashimi_tanpopoでは下記のようにglob記法に対応することで複数ファイルに対応しました。

update_file ".github/workflows/*.yml" do |content|
  content.gsub!(/ruby-version: "(.+)"/, %Q{ruby-version: "#{params[:ruby_version]}"})
end

実行時引数で渡せるようにした

動的にパラメータを渡したかったのでsashimi_tanpopoでは下記のように実行時のパラメータとして自然に渡せるようにしました。

sashimi_tanpopo github --params ruby_version:3.4

余談: key:value って記法は個人的に若干違和感あるんですが、https://github.com/rails/thor がそういう仕様なので踏襲した感じです。 *4

その他の工夫ポイント

レシピファイル内で普通にRubyが書ける

レシピファイル(ファイルを編集するルールを定義したファイル)は普通のRubyファイルなのでRubyの文法でコードが書けます。

下記のようにレシピファイル内でローカル変数やインスタンスやメソッドを定義することでいい感じにすることができます。 *5

def ruby_version_with_patch_level(ruby_version)
  v = ruby_version.split(".")
  return nil unless v.size == 3

  # Fetch RUBY_PATCHLEVEL from https://github.com/ruby/ruby/blob/master/version.h
  git_tag = "v" + ruby_version.gsub(".", "_")
  version_h = URI.open("https://raw.githubusercontent.com/ruby/ruby/#{git_tag}/version.h").read
  ruby_patchlevel = /^#define\s+RUBY_PATCHLEVEL\s+(\d+)/.match(version_h).to_a[1]

  "#{ruby_version}p#{ruby_patchlevel}"
end

v = params[:ruby_version].split(".")

@is_full_version = v.count == 3
@ruby_minor_version = "#{v[0]}.#{v[1]}"
@gcp_runtime_version = "ruby#{v[0]}#{v[1]}"
@ruby_version_with_patch_level = ruby_version_with_patch_level(params[:ruby_version])

update_file ".rubocop.yml" do |content|
  content.gsub!(/TargetRubyVersion: ([\d.]+)/, "TargetRubyVersion: #{@ruby_minor_version}")
end

CIで使いやすくした

冒頭の例を見ただけだと大したことないかと思うかもしれないですが、sashimi_tanpopoはCIで動かすことで真価を発揮します。

こういうファイル を編集するだけで複数のリポジトリに対してPull RequestやMerge Requestをいい感じに作ることができます。

自分がメンテしてるOSSに関してはある程度sashimi_tanpopoでメンテできるようになったので詳しくはこれを見てください。

昨今ではOSSでもGitHub ActionsやGitLab CIで使いやすくするような再利用可能なコンポーネントを提供することが多いため、それぞれのCIサービス用にコンポーネントを作りました。

余談

名前について

いい感じの名前を思いつくまでが一番大変でしたw

いくつかあった候補の中で一番しっくりきたのがsashimi_tanpopoだったのでこの名前にしました

余談ですが他の命名候補は下記でした。

  • sashitan(刺し身タンポポの略)
  • kobitosan(小人さんが寝てる間に仕事をしてくれるイメージ)
  • senju_kannon(千手観音のようにファイルを編集するイメージ)

リポジトリをたくさん作った

今回はgem本体以外にも色々リポジトリを作ったので色々大変でした...(GitHubとGitLabで計6つ)

GitHub *6

GitLab

Go Conference 2025に参加した #gocon

参加してきました。Proposal落ちたので一般参加です。

gocon.jp

気になったセッション

サプライチェーン攻撃に学ぶmoduleの仕組みとセキュリティ対策

gocon.jp

昨今サプライチェーンアタックが(悪い意味で)盛り上がってるし、自分自身会社でこの手のセキュリティ対応することも多いので興味深かったです。

とりあえず自分がメンテしてるGo系OSSにはだいたい govulncheck を導入しました。

一通り入れたところで1件だけ検知されたので修正した。

GitHub Actionsで使う場合には https://github.com/golang/govulncheck-action が便利でした。

Go で WebAssembly を利用した実用的なプラグインシステムの構築方法

gocon.jp

こういう泥臭い話好き

analysis パッケージの仕組みの上でMulti linter with configを実現する

gocon.jp

最近 https://github.com/k1LoW/deck を使い始めたのでその作者の人の話ということで気になってた。

トークの冒頭でOSSを300以上作ってるって話していて、思わずAsk the Speakerで「自分もたくさんOSSメンテしてて複数のリポジトリでGoのバージョンを上げたりdependabot.yml編集して回るのだいぶつらいと思うんですがなにか工夫してますか?」って聞きに行ってしまいました。(「特に工夫はやってない、必要に応じて上げる」って回答だった)

私達はmodernize packageに夢を見るか feat. go/analysis, go/ast

gocon.jp

RuboCopのカスタムcopを作ってるのでGoでASTを使って静的解析したりソースコードを書き換える仕組みが知れて面白かったです。

公式singleflightを約2倍高速化したGenerics対応実装と最適化

gocon.jp

sf.m の削除処理をgoroutineの中でやるのが目から鱗だったです。この手のテクは他でも使えそうなので覚えておきたいです。

全体を通しての所感

僕はよくRubyKaigiに参加するのでどうしてもそこと比較しがちになるんですが、Go ConferenceだとGo言語の深い仕様や機能やツールに関する紹介を 三者 話していたのが多かった印象です。(RubyKaigiだとだいたいその機能やツールを作った本人が喋るため)

どっちがいいとか悪いとかではなく、カンファレンスによってそういう色の違いがあるのだなぁという雑な感想でした。

他の人が作った仕様や機能を深く理解したり、議論の流れをウォッチするのも努力の結果なのですごいと思います。

とはいえ自作ツールの開発を通したGo言語の内部実装のトークもあったので、今回僕のgoconのProposal(go-gem-wrapperの開発を通したcgoやCのマニアックな話)が落ちたのはその辺の違いかなぁと思いました...

そういえば落ちたProposalを公開するのを忘れてたのでこの場を借りて放流しようと思います。

https://esa-pages.io/p/sharing/8985/posts/999/a1604a5569af0926256a.html

【ネタ】Gopher Sponsorになった

僕の名前がGoなのでGopher Sponsorになりました。手頃な金額でこういうネタ仕込めるのお得感ありますね。

「ペパボ & GO 〜 夏のGo祭り2025、あの夏〜」でLT登壇した #gogopepabo

pepabo.connpass.com

自分の名前がGoなので登壇しなければ(使命感)という気持ちになってLTしました。

Go(言語)の勉強会でGO(株式会社)の人がいる前で「Go(自分の名前)歴43年です」って自己紹介したのが一番の面白ポイントです。

発表資料

speakerdeck.com

Go Conference 2025 に出して落ちたプロポーザルを頑張って5分にまとめました。

余談

gitlabci-bundle-update-mrでCI/CD componentを提供するようにした

前置き

CI/CD componentというのはこれのこと。

docs.gitlab.com

GitLab CIでは昔からリポジトリの外部のファイルをincludeしてjobとして利用できる機能があって、 https://gitlab.com/sue445/gitlabci-bundle-update-mr でも https://gitlab.com/sue445/gitlabci-bundle-update-mr/-/blob/master/gitlabci-templates/continuous_bundle_update.yml?ref_type=heads のようなテンプレートファイルとして提供していました。

しかしこのテンプレートの中で使ってるonlyキーワードがだいぶ前からdeprecatedになっていました。 *1

いい加減なんとかするかと思って現行のrulesに移行するついでにCI/CD componentを提供するようにしました

CI/CD componentだとinputsで明示的にパラメータを宣言できたり、array typeでrules全体を引数として渡せるのがいいですね。

自分がメンテしてるDockerイメージをslimに移行した

タイトルが全て

経緯

勢い

やったこと

ruby:3.4 を使ってるリポジトリでベースイメージを ruby:3.4-slim に変えた。

github.com

github.com

結果

push後のサイズが半分くらいになっていい感じ

もっとDockerイメージダイエットを頑張るのであればalpineにすべきなんだけど https://future-architect.github.io/articles/20240726a/

Alpineを選んで25MB変わったところでストレージのコストや転送時間は対して変わりませんが、CPU性能が落ちて、その分余計に処理時間が伸びて増えるコストの方が膨大なので、Debian系のベースイメージを使うべきです。

ってあるのでDebianのままにしてます