Slide 1

Slide 1 text

"複雑なデータ処理 × 静的サイト" を両立させ る、楽をするRails運用 Railsで作られた静的サイト Ruby Contributors の開発・運用

Slide 2

Slide 2 text

自己紹介 hogelog (GitHub, X, …) STORES VPoE 今日話すのは仕事と関係ない、趣味の話です We are hiring, hiring, HIRING!!! https://jobs.st.inc/ プログラミングやRubyが割と好きです

Slide 3

Slide 3 text

Kaigi on Rails と自分

Slide 4

Slide 4 text

アジェンダ Ruby Contributorsの紹介 開発のきっかけとぶつかった困難 Rails活用アプローチの詳細 うれしかったこと

Slide 5

Slide 5 text

Ruby Contributors https://rubycontributors.pages.dev/

Slide 6

Slide 6 text

Ruby Contributors とは CRubyコントリビュータを可視化 github.com/ruby/ruby を解析 コントリビュータ毎のコミットログ GitHub ユーザと紐付け ランキング(週/月/年/全期間) https://rubycontributors.pages.dev/ https://github.com/hogelog/rubycontributors

Slide 7

Slide 7 text

開発のきっかけ 2023年末頃の自分のCRubyへのコントリビュート 自分のCRubyコントリビュートを眺めたい Rails Contributorsのようにコントリビュートを可視化したい rubycontributors.pages.dev/contributors/hogelog

Slide 8

Slide 8 text

ぶつかった困難 適当に書き散らしたスクリプトだけでは辛かった git fetch と GitHub を叩くスクリプトが肥大化 データは巨大なJSONLとログファイルとして保存 リポジトリにコミットして管理 同一人物判定が割と複雑 Subversion時代のログのAuthorとGit移行後のログのAuthorとの突合 メールアドレスが複数ある人の突合 GitHubユーザが複数ある人の突合 名前が複数ある人の突合 悩んでいるうちに開発が途切れてしまった

Slide 9

Slide 9 text

ふと思いついたRails活用アプローチ RubyKaigi 2025 を控えた2025年3月、Ruby Contributors開発を再開 「データと処理の構造化に悩むなら、Railsで整理すればいいのでは?」 やってみた ✅ ActiveRecordでデータモデリング → スムーズに整理できた ✅ 複雑な処理もMVCで構造化 → コードが読み書きしやすく ✅ 同一人物判定 → 最後は管理画面での人間が判断する運用に割り切り

Slide 10

Slide 10 text

アプローチの詳細 Railsで処理とデータを構造化していく

Slide 11

Slide 11 text

Ruby Contributors 全体アーキテクチャ 1. データ取得 git fetch コミットログ取得 GitHub API GitHubユーザ取得 2. データの加工 ActiveRecord データの正規化 Action Conroller + Action View 静的HTMLの生成 ActiveJob + Rake スケジュール実行 管理画面のみ動的Rails 3. 静的サイト配信 Cloudflare Pagesで公開 rubycontributors.pages.dev

Slide 12

Slide 12 text

本番運用環境の構成 静的サイト配信 VPS GitHub Rails App git fetch Commit Info deploy ruby/ruby repository GitHub API rake deploy:import データ取得 rake deploy:pages サイト生成 管理画面 SQLite スケジュールバッチ 12 時間ごと Cloudflare Pages 一般ユーザ

Slide 13

Slide 13 text

元データ: GitHubコミットログ ruby/rubyコミットログ 数万のコミット 100人以上のコントリビュータ 同一人物の複数の名前、メールアドレス、GitHubユーザの統合 # ruby/rubyリポジトリから取得 git fetch && git log 392296c..FETCH_HEAD --pretty=format:"%H\t%at\t%an\t%ae\t%s" # GitHub APIで追加情報取得 curl https://github.com/ruby/ruby/commit/[sha].json

Slide 14

Slide 14 text

Railsでのモデル構造 has_many has_many has_many has_many Contributor int id datetime created_at datetime updated_at Commit int id string sha datetime time string name string email string message int contributor_id ContributorLogin int id string login int contributor_id ContributorName int id string name int contributor_id ContributorEmail int id string email int contributor_id class Commit < ApplicationRecord belongs_to :contributor end class Contributor < ApplicationRecord has_many :commits has_many :contributor_logins has_many :contributor_names has_many :contributor_emails end class ContributorLogin < ApplicationRecord belongs_to :contributor end class ContributorName < ApplicationRecord belongs_to :contributor end class ContributorEmail < ApplicationRecord belongs_to :contributor end

Slide 15

Slide 15 text

コミットログを取り込む Active Job class CommitImportJob < ApplicationJob def perform commit_logs = [] revision_range = Commit.last&.sha ? "#{Commit.last.sha}..FETCH_HEAD" : "FETCH_HEAD" Dir.chdir(RUBY_REPO_DIR) do system("git fetch", exception: true) IO.popen(%[git log #{revision_range} --pretty=format:"%H\t%at\t%an\t%ae\t%s"], exception: true) do |io| io.each_line do |line| commit_logs << line.chomp.split("\t") end end end commit_logs.reverse_each do |sha, timestamp, name, email, message| next if Commit.exists?(sha:) time = Time.at(timestamp.to_i) contributor = Contributor.find_or_upsert_by_commit!(sha:, name:, email:) unless contributor.commits.exists?(sha:) Commit.create!(sha:, time:, name:, email:, message:, contributor_id: contributor.id) end end

Slide 16

Slide 16 text

コントリビュータを引いてくる or 作成する Active Record モデル class Contributor < ApplicationRecord ... def self.find_or_upsert_by_commit!(sha:, name:, email:) contributor_email = ContributorEmail.find_by(email:) # if known email if contributor_email # Found existing contributor by email contributor = contributor_email.contributor # Add new name if not exists contributor.contributor_names.find_or_create_by!(name:) return contributor end # Fetch GitHub login information login = Github.fetch_login(sha, name) if login # Search for existing contributor by login contributor_login = ContributorLogin.find_by(login:) if contributor_login contributor = contributor_login.contributor else # Create new contributor with login contributor = create! contributor.contributor_logins.create!(login:) end else # Create new contributor without login contributor = create! end # Add email and name contributor.contributor_emails.create!(email:) contributor.contributor_names.find_or_create_by!(name contributor end ... end

Slide 17

Slide 17 text

コントリビュータを引いてくる or 作成するロジックのフローチャート 見つかった 見つからない 未登録 登録済み あり なし 見つかった 見つからない 未登録 登録済み コミット情報 sha, name, email メールアドレスで 既存コントリビュータ検索 既存コントリビュータ を取得 この名前は 既に登録済み? 名前を追加 コントリビュータを返却 GitHub から ログイン情報を取得 ログイン情報 取得できた? ログインで 既存コントリビュータ検索 既存コントリビュータ を取得 新規コントリビュータ を作成 ログイン情報を 追加 新規コントリビュータ を作成 メールアドレスを 追加 この名前は 既に登録済み? 名前を追加 コントリビュータを返却

Slide 18

Slide 18 text

静的HTMLの生成をする Active Job (コントリビュータのコミット一覧HTML生成部分) class SiteGenerateJob < ApplicationJob def perform ... contributors = Contributor .select("contributors.*, COUNT(commits.id) as commits_count") .left_joins(:commits) .group(:id) .order(commits_count: :desc) .preload(:contributor_names, :contributor_emails, :contributor_logins, :commits) contributors.each do |contributor| show_html = Static::ContributorsController.renderer.render_to_string( template: "static/contributors/show", locals: { contributor: } ) html_path = "tmp/public/contributors/#{contributor.name_path}.html" File.write(html_path, show_html) end ...

Slide 19

Slide 19 text

静的HTMLを生成する Action Controller class Static::ContributorsController < ApplicationController def index contributors = Contributor .select("contributors.*, COUNT(commits.id) as commits_count") .left_joins(:commits) .group(:id) .order(commits_count: :desc) .preload(:contributor_names, :contributor_emails, :contributor_logins, :commits) .reject {|contributor| contributor.bot? } render "static/contributors/index", locals: { contributors: } end def show contributor = ContributorName.find_by_name!(params[:id]).contributor render "static/contributors/show", locals: { contributor: } end end

Slide 20

Slide 20 text

静的HTMLを生成する Action View ERB テンプレート

<%= contributor.names.join(", ") %> <% if contributor.bot? %> <% contributor.logins.each do |login| %> <%= link_to "https://github.com/#{login}", class: "text-gray-900 hover:text-gray-700 hover:underline mr-2 i GitHub @<%= login %> <% end %> <% end %>
<% end %>

Slide 21

Slide 21 text

Active Job の起動とデプロイをトリガーするだけのRakeタスク namespace :deploy do desc "Import commits" task :import do sh "bundle exec rails runner CommitImportJob.perform_now" end desc "Deploy to pages" task :pages do sh "bundle exec rails runner SiteGenerateJob.perform_now" sh "npm run deploy" end end

Slide 22

Slide 22 text

Ruby Contributors に使ったRailsのパーツ Active Record データモデリング SQLiteで利用 Action Controller 静的HTML生成: render_to_string Action View ERBテンプレート レイアウト管理 Active Job データ取り込み 静的HTML生成 Rake Active Jobの起動 Cloudflare Pagesへのデプロイ 管理画面 管理画面は「ふつうの」動的なRails 一般利用者はアクセスできない

Slide 23

Slide 23 text

なぜRailsが効果的だったか データ構造の整理が得意 ActiveRecordで複雑なリレーションを直感的に表現 MVCを基本とした構造化で読み書きしやすく DB (SQLite) を導入しコードとデータを分離 人間が介入できる仕組み 管理画面で複雑な同一人物判定を人手で処理 ロジックで解決困難な問題を運用でカバー スケジュールバッチ実行の仕組み Active Job + Rakeタスクで処理

Slide 24

Slide 24 text

素のRailsだけで組み立てる https://github.com/hogelog/rubycontributors/blob/main/Gemfile source "https://rubygems.org" gem "rails" gem "sqlite3" gem "puma" gem "bootsnap", require: false group :development, :test do gem "debug" gem "brakeman", require: false end group :development do gem "web-console" end

Slide 25

Slide 25 text

うれしかったこと 実際に運用してみて

Slide 26

Slide 26 text

公開サイトは静的なので安心 セキュリティリスクが最小限 データベースやサーバサイド処理は非公開 攻撃対象は静的ファイルのみ スケーラビリティも安心 アクセス増もCDNが対応 重い監視やチューニングはほぼ不要

Slide 27

Slide 27 text

個人開発にちょうどいい運用負荷 「盆栽Railsアプリ」として最適 趣味プロジェクトとして続けやすいサイズ感 動的サイト運用ほどは重くない 忙しい時期でも放置できる安心感 最新Railsを試す実験場として 新機能を気軽に試せる

Slide 28

Slide 28 text

✅ こんな人におすすめ 静的サイトで運用コストを抑えたい 静的なのだが複雑なデータ処理が必要なサイト Railsの使い方には慣れている・慣れるぞという気持ちのある方 (かもしれない)

Slide 29

Slide 29 text

まとめ "ビルド時だけRailsの力を借りて、公開は静的ファイルだけ" Railsの開発生産性 × 静的サイトの運用効率 複雑なデータ処理でもActiveRecordで直感的に実装 運用コスト・セキュリティリスクを大幅削減 Ruby Contributors サイト: https://rubycontributors.pages.dev/ GitHub: https://github.com/hogelog/rubycontributors 資料: https://speakerdeck.com/hogelog/a-low-effort-rails-workflow-that- combines-complex-data-processing-x-static-sites STORES: We are hiring, hiring, HIRING!!! https://jobs.st.inc/