Upgrade to Pro — share decks privately, control downloads, hide ads and more …

"複雑なデータ処理 × 静的サイト" を両立させる、楽をするRails運用 / A low-e...

Avatar for hogelog hogelog
September 27, 2025

"複雑なデータ処理 × 静的サイト" を両立させる、楽をするRails運用 / A low-effort Rails workflow that combines “Complex Data Processing × Static Sites”

Kaigi on Rails 2025 Day 2 2025/09/27発表、"複雑なデータ処理 × 静的サイト" を両立させる、楽をするRails運用

Avatar for hogelog

hogelog

September 27, 2025
Tweet

More Decks by hogelog

Other Decks in Technology

Transcript

  1. 自己紹介 hogelog (GitHub, X, …) STORES VPoE 今日話すのは仕事と関係ない、趣味の話です We are

    hiring, hiring, HIRING!!! https://jobs.st.inc/ プログラミングやRubyが割と好きです
  2. ぶつかった困難 適当に書き散らしたスクリプトだけでは辛かった git fetch と GitHub を叩くスクリプトが肥大化 データは巨大なJSONLとログファイルとして保存 リポジトリにコミットして管理 同一人物判定が割と複雑

    Subversion時代のログのAuthorとGit移行後のログのAuthorとの突合 メールアドレスが複数ある人の突合 GitHubユーザが複数ある人の突合 名前が複数ある人の突合 悩んでいるうちに開発が途切れてしまった
  3. ふと思いついたRails活用アプローチ RubyKaigi 2025 を控えた2025年3月、Ruby Contributors開発を再開 「データと処理の構造化に悩むなら、Railsで整理すればいいのでは?」 やってみた ✅ ActiveRecordでデータモデリング →

    スムーズに整理できた ✅ 複雑な処理もMVCで構造化 → コードが読み書きしやすく ✅ 同一人物判定 → 最後は管理画面での人間が判断する運用に割り切り
  4. Ruby Contributors 全体アーキテクチャ 1. データ取得 git fetch コミットログ取得 GitHub API

    GitHubユーザ取得 2. データの加工 ActiveRecord データの正規化 Action Conroller + Action View 静的HTMLの生成 ActiveJob + Rake スケジュール実行 管理画面のみ動的Rails 3. 静的サイト配信 Cloudflare Pagesで公開 rubycontributors.pages.dev
  5. 本番運用環境の構成 静的サイト配信 VPS GitHub Rails App git fetch Commit Info

    deploy ruby/ruby repository GitHub API rake deploy:import データ取得 rake deploy:pages サイト生成 管理画面 SQLite スケジュールバッチ 12 時間ごと Cloudflare Pages 一般ユーザ
  6. 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
  7. コミットログを取り込む 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
  8. コントリビュータを引いてくる 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
  9. コントリビュータを引いてくる or 作成するロジックのフローチャート 見つかった 見つからない 未登録 登録済み あり なし 見つかった

    見つからない 未登録 登録済み コミット情報 sha, name, email メールアドレスで 既存コントリビュータ検索 既存コントリビュータ を取得 この名前は 既に登録済み? 名前を追加 コントリビュータを返却 GitHub から ログイン情報を取得 ログイン情報 取得できた? ログインで 既存コントリビュータ検索 既存コントリビュータ を取得 新規コントリビュータ を作成 ログイン情報を 追加 新規コントリビュータ を作成 メールアドレスを 追加 この名前は 既に登録済み? 名前を追加 コントリビュータを返却
  10. 静的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 ...
  11. 静的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
  12. 静的HTMLを生成する Action View ERB テンプレート <div class="max-w-7xl mx-auto"> <div class="mb-8">

    <h1 class="text-2xl font-bold text-gray-900"> <div class="flex items-center gap-2"> <%= contributor.names.join(", ") %> <% if contributor.bot? %> <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-80 Bot </span> <% end %> </div> <% if contributor.logins.any? %> <div class="mt-2 text-sm text-gray-600"> <% contributor.logins.each do |login| %> <%= link_to "https://github.com/#{login}", class: "text-gray-900 hover:text-gray-700 hover:underline mr-2 i <img src="/github.svg" class="w-4 h-4 mr-1" alt="GitHub"> @<%= login %> <% end %> <% end %> </div> <% end %> </h1>
  13. 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
  14. Ruby Contributors に使ったRailsのパーツ Active Record データモデリング SQLiteで利用 Action Controller 静的HTML生成:

    render_to_string Action View ERBテンプレート レイアウト管理 Active Job データ取り込み 静的HTML生成 Rake Active Jobの起動 Cloudflare Pagesへのデプロイ 管理画面 管理画面は「ふつうの」動的なRails 一般利用者はアクセスできない
  15. 素の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
  16. まとめ "ビルド時だけ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/