Skip to content

Instantly share code, notes, and snippets.

@3zcurdia
Last active August 13, 2026 17:43
Show Gist options
  • Select an option

  • Save 3zcurdia/86622932145a3375eace2bc3d2dfe24a to your computer and use it in GitHub Desktop.

Select an option

Save 3zcurdia/86622932145a3375eace2bc3d2dfe24a to your computer and use it in GitHub Desktop.
GitHub PR report
#!/usr/bin/env ruby
# frozen_string_literal: true
# pr_summary.rb
# Usage: gh-report <org/repo>
#
# Dependencies:
# gh CLI installed and authenticated → gh auth login
# Ollama running locally → ollama serve && ollama pull qwen2.5:9b
require "bundler/inline"
gemfile do
source "https://rubygems.org"
gem "ruby_llm"
end
require "json"
require "date"
# ── Value objects ─────────────────────────────────────────────────────────────
class PullRequest
attr_reader :number, :title, :body, :url, :state, :created_at, :merged_at
def initialize(number:, title:, body:, url:, state:, created_at:, merged_at:)
@number = number
@title = title
@body = body
@url = url
@state = state
@created_at = created_at
@merged_at = merged_at
end
def self.from_hash(h)
new(
number: h["number"],
title: h["title"],
body: h["body"].to_s.strip,
url: h["url"],
state: h["state"],
created_at: h["createdAt"],
merged_at: h["mergedAt"]
)
end
def day
Date.parse(created_at).to_s
end
def state_icon
case state
when "MERGED" then "✅"
when "OPEN" then "🔵"
else "⛔"
end
end
end
class DaySummary
attr_reader :date_str, :prs, :summary
def initialize(date_str:, prs:, summary:)
@date_str = date_str
@prs = prs
@summary = summary
end
def date_label
Date.parse(date_str).strftime("%A, %B %-d %Y")
end
end
# ── GitHubClient ──────────────────────────────────────────────────────────────
class GitHubClient
class Error < StandardError; end
def initialize(repo)
@repo = repo
end
def current_user
output = `gh api user --jq '.login' 2>/dev/null`.strip
output.empty? ? nil : output
end
def cli_available?
system("gh --version > /dev/null 2>&1")
end
def fetch_prs(author, since_date)
cmd = [
"gh pr list",
"--repo #{@repo}",
"--author #{author}",
"--state all",
"--limit 100",
"--json number,title,body,url,createdAt,mergedAt,state",
"--search 'created:>=#{since_date}'"
].join(" ")
raw = `#{cmd} 2>/dev/null`
return [] if raw.strip.empty?
JSON.parse(raw).map { |h| PullRequest.from_hash(h) }
rescue JSON::ParserError
[]
end
def fetch_commits(pr_number)
raw = `gh pr view #{pr_number} --repo #{@repo} --json commits --jq '.commits[].messageHeadline' 2>/dev/null`
raw.strip.split("\n").reject(&:empty?)
rescue StandardError
[]
end
end
# ── Summarizer ────────────────────────────────────────────────────────────────
class Summarizer
def initialize
RubyLLM.configure do |config|
config.ollama_api_base = "http://localhost:1234/v1"
config.ollama_api_key = "dummy-key"
config.default_model = "google/gemma-4-e4b"
end
end
def summarize(date_str, pr_commit_pairs)
prompt = build_prompt(date_str, pr_commit_pairs)
chat = RubyLLM.chat(model: "google/gemma-4-e4b", provider: :ollama)
chat.ask(prompt).content.strip
rescue StandardError => e
"⚠️ Could not generate summary: #{e.message}"
end
private
def build_prompt(date_str, pr_commit_pairs)
context = pr_commit_pairs.map do |pr, commits|
lines = ["PR ##{pr.number}: #{pr.title}", " Status: #{pr.state}"]
lines << " Description: #{pr.body[0, 600]}" unless pr.body.empty?
unless commits.empty?
lines << " Commits:"
commits.each { |c| lines << " - #{c}" }
end
lines.join("\n")
end.join("\n\n")
<<~PROMPT
You are writing a short daily update for an executive audience.
Goal:
Summarize what was accomplished today based on the provided pull requests.
Instructions:
- Write in third-person passive voice using action verbs (e.g., "Implemented...", "Fixed...", "Improved...").
- Do NOT use any pronouns (no "I", "we", "they", etc.).
- Use past tense throughout.
- Use plain, simple English. Avoid technical jargon or explain it briefly.
- Be concise and direct.
Input:
Date: #{date_str}
Work:
#{context}
Output:
- 2 to 3 bullet points only.
- Start each bullet with a past-tense action verb (e.g., "Implemented", "Fixed", "Improved", "Resolved", "Enabled", "Reduced").
- Do not mention pull requests, commits, or code.
- Emphasize impact (e.g., improved performance, fixed issues, enabled features, reduced risk).
Example style:
"- Improved the reliability of the payment system, reducing errors during checkout.
- Fixed a bug that was causing delays for some users.
- Enabled a new feature that streamlines the onboarding process."
PROMPT
end
end
# ── Renderer ──────────────────────────────────────────────────────────────────
class Renderer
BOLD = "\e[1m"
CYAN = "\e[36m"
GREEN = "\e[32m"
DIM = "\e[2m"
RESET = "\e[0m"
def print_header(repo)
puts
puts "#{BOLD}#{CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━#{RESET}"
puts "#{BOLD} PR Summary — #{repo}#{RESET}"
puts "#{BOLD}#{CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━#{RESET}"
puts
end
def print_day(day_summary)
puts "#{BOLD}#{GREEN}📅 #{day_summary.date_label}#{RESET}"
puts
puts day_summary.summary
puts
puts "#{DIM} PRs:#{RESET}"
day_summary.prs.each do |pr|
puts " #{pr.state_icon} ##{pr.number} #{pr.title}"
puts " #{DIM}#{pr.url}#{RESET}"
end
puts
puts "#{DIM}#{'-' * 56}#{RESET}"
puts
end
def print_done
puts "#{BOLD}Done!#{RESET}"
end
end
# ── App ───────────────────────────────────────────────────────────────────────
class App
def initialize(repo)
@repo = repo
@client = GitHubClient.new(repo)
@summarizer = Summarizer.new
@renderer = Renderer.new
end
def run
validate_cli!
author = resolve_author!
since_date = (Date.today - 7).to_s
puts "🔍 Fetching PRs for #{author} in #{@repo} since #{since_date}…"
prs = @client.fetch_prs(author, since_date)
if prs.empty?
puts "No pull requests found in the last 7 days for #{author} in #{@repo}."
return
end
puts "📥 Found #{prs.length} PR(s). Fetching commit details…"
pr_commit_pairs = prs.map { |pr| [pr, @client.fetch_commits(pr.number)] }
day_summaries = build_day_summaries(pr_commit_pairs)
@renderer.print_header(@repo)
day_summaries.each { |ds| @renderer.print_day(ds) }
@renderer.print_done
end
private
def validate_cli!
abort "❌ GitHub CLI (gh) is not installed or not in PATH." unless @client.cli_available?
end
def resolve_author!
author = @client.current_user
abort "❌ Could not determine your GitHub username. Run: gh auth login" if author.nil? || author.empty?
author
end
def build_day_summaries(pr_commit_pairs)
grouped = pr_commit_pairs
.group_by { |pr, _| pr.day }
.sort
.to_h
grouped.map do |date_str, pairs|
print "✨ Summarizing #{date_str}… "
$stdout.flush
summary = @summarizer.summarize(date_str, pairs)
puts "done."
DaySummary.new(date_str: date_str, prs: pairs.map(&:first), summary: summary)
end
end
end
# ── Entry point ───────────────────────────────────────────────────────────────
unless ARGV.length == 1 && ARGV[0].include?("/")
warn "Usage: ruby pr_summary.rb <org/repo>"
warn "Example: ruby pr_summary.rb rails/rails"
exit 1
end
App.new(ARGV[0].strip).run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment