summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTakashi Kokubun <takashi.kokubun@shopify.com>2025-10-03 23:09:43 -0700
committerGitHub <noreply@github.com>2025-10-03 23:09:43 -0700
commitecc5ebc69a76f3a267a90b9af3d6754b3cc21265 (patch)
tree4936fda2843ed5af7e856d93362eff6be2c705f7
parent5941659e9b860a0a4bc4c64ef990f05a9dcebbe6 (diff)
Migrate notes-github-pr to ruby/ruby (#14725)
from ruby/git.ruby-lang.org as of: https://github.com/ruby/git.ruby-lang.org/commit/f3ed893e946ec66cac77af5859ac879c5983d3a3
-rw-r--r--.github/workflows/check_misc.yml7
-rw-r--r--tool/notes-github-pr.rb146
2 files changed, 153 insertions, 0 deletions
diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml
index 5ff6c0b5b0..d4da4ecc27 100644
--- a/.github/workflows/check_misc.yml
+++ b/.github/workflows/check_misc.yml
@@ -132,6 +132,13 @@ jobs:
name: ${{ steps.docs.outputs.htmlout }}
if: ${{ steps.docs.outcome == 'success' }}
+ - name: Push PR notes to GitHub
+ run: ruby tool/notify-github-pr.rb "$(pwd)" "$GITHUB_OLD_SHA" "$GITHUB_NEW_SHA" refs/heads/master
+ env:
+ GITHUB_OLD_SHA: ${{ github.event.before }}
+ GITHUB_NEW_SHA: ${{ github.event.after }}
+ if: ${{ github.repository == 'ruby/ruby' && github.ref == 'refs/heads/master' && github.event_name == 'push' }}
+
- uses: ./.github/actions/slack
with:
SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot
diff --git a/tool/notes-github-pr.rb b/tool/notes-github-pr.rb
new file mode 100644
index 0000000000..bc888f548e
--- /dev/null
+++ b/tool/notes-github-pr.rb
@@ -0,0 +1,146 @@
+#!/usr/bin/env ruby
+# Add GitHub pull request reference / author info to git notes.
+
+require 'net/http'
+require 'uri'
+require 'tmpdir'
+require 'json'
+require 'yaml'
+
+# Conversion for people whose GitHub account name and SVN_ACCOUNT_NAME are different.
+GITHUB_TO_SVN = {
+ 'amatsuda' => 'a_matsuda',
+ 'matzbot' => 'git',
+ 'jeremyevans' => 'jeremy',
+ 'znz' => 'kazu',
+ 'k-tsj' => 'ktsj',
+ 'nurse' => 'naruse',
+ 'ioquatix' => 'samuel',
+ 'suketa' => 'suke',
+ 'unak' => 'usa',
+}
+
+SVN_TO_EMAILS = YAML.safe_load(File.read(File.expand_path('../config/email.yml', __dir__)))
+
+class GitHub
+ ENDPOINT = URI.parse('https://api.github.com')
+
+ def initialize(access_token)
+ @access_token = access_token
+ end
+
+ # https://developer.github.com/changes/2019-04-11-pulls-branches-for-commit/
+ def pulls(owner:, repo:, commit_sha:)
+ resp = get("/repos/#{owner}/#{repo}/commits/#{commit_sha}/pulls", accept: 'application/vnd.github.groot-preview+json')
+ JSON.parse(resp.body)
+ end
+
+ # https://developer.github.com/v3/pulls/#get-a-single-pull-request
+ def pull_request(owner:, repo:, number:)
+ resp = get("/repos/#{owner}/#{repo}/pulls/#{number}")
+ JSON.parse(resp.body)
+ end
+
+ # https://developer.github.com/v3/users/#get-a-single-user
+ def user(username:)
+ resp = get("/users/#{username}")
+ JSON.parse(resp.body)
+ end
+
+ private
+
+ def get(path, accept: 'application/vnd.github.v3+json')
+ Net::HTTP.start(ENDPOINT.host, ENDPOINT.port, use_ssl: ENDPOINT.scheme == 'https') do |http|
+ headers = { 'Accept': accept, 'Authorization': "bearer #{@access_token}" }
+ http.get(path, headers).tap(&:value)
+ end
+ end
+end
+
+module Git
+ class << self
+ def abbrev_ref(refname, repo_path:)
+ git('rev-parse', '--symbolic', '--abbrev-ref', refname, repo_path: repo_path).strip
+ end
+
+ def rev_list(arg, first_parent: false, repo_path: nil)
+ git('rev-list', *[('--first-parent' if first_parent)].compact, arg, repo_path: repo_path).lines.map(&:chomp)
+ end
+
+ def commit_message(sha)
+ git('log', '-1', '--pretty=format:%B', sha)
+ end
+
+ def notes_message(sha)
+ git('log', '-1', '--pretty=format:%N', sha)
+ end
+
+ def committer_name(sha)
+ git('log', '-1', '--pretty=format:%cn', sha)
+ end
+
+ def committer_email(sha)
+ git('log', '-1', '--pretty=format:%cE', sha)
+ end
+
+ private
+
+ def git(*cmd, repo_path: nil)
+ env = {}
+ if repo_path
+ env['GIT_DIR'] = repo_path
+ end
+ out = IO.popen(env, ['git', *cmd], &:read)
+ unless $?.success?
+ abort "Failed to execute: git #{cmd.join(' ')}\n#{out}"
+ end
+ out
+ end
+ end
+end
+
+github = GitHub.new(ENV.fetch("GITHUB_TOKEN"))
+
+repo_path, *rest = ARGV
+rest.each_slice(3).map do |oldrev, newrev, refname|
+ branch = Git.abbrev_ref(refname, repo_path: repo_path)
+ next if branch != 'master' # we use pull requests only for master branches
+
+ Dir.mktmpdir do |workdir|
+ # Clone a branch and fetch notes
+ depth = Git.rev_list("#{oldrev}..#{newrev}", repo_path: repo_path).size + 50
+ system('git', 'clone', "--depth=#{depth}", "--branch=#{branch}", "file://#{repo_path}", workdir)
+ Dir.chdir(workdir)
+ system('git', 'fetch', 'origin', 'refs/notes/commits:refs/notes/commits')
+
+ updated = false
+ Git.rev_list("#{oldrev}..#{newrev}", first_parent: true).each do |sha|
+ github.pulls(owner: 'ruby', repo: 'ruby', commit_sha: sha).each do |pull|
+ number = pull.fetch('number')
+ url = pull.fetch('html_url')
+ next unless url.start_with?('https://github.com/ruby/ruby/pull/')
+
+ # "Merged" notes for "Squash and merge"
+ message = Git.commit_message(sha)
+ notes = Git.notes_message(sha)
+ if !message.include?(url) && !message.match(/[ (]##{number}[) ]/) && !notes.include?(url)
+ system('git', 'notes', 'append', '-m', "Merged: #{url}", sha)
+ updated = true
+ end
+
+ # "Merged-By" notes for "Rebase and merge"
+ if Git.committer_name(sha) == 'GitHub' && Git.committer_email(sha) == 'noreply@github.com'
+ username = github.pull_request(owner: 'ruby', repo: 'ruby', number: number).fetch('merged_by').fetch('login')
+ email = github.user(username: username).fetch('email')
+ email ||= SVN_TO_EMAILS[GITHUB_TO_SVN.fetch(username, username)]&.first
+ system('git', 'notes', 'append', '-m', "Merged-By: #{username}#{(" <#{email}>" if email)}", sha)
+ updated = true
+ end
+ end
+ end
+
+ if updated
+ system('git', 'push', 'origin', 'refs/notes/commits')
+ end
+ end
+end