Skip to content

Commit cefa5f6

Browse files
authored
Merge pull request #77 from Shopify/rwstauner/create-command
Add rake task to automate pshopify definition creation
2 parents 5768242 + 834e8c8 commit cefa5f6

File tree

3 files changed

+210
-0
lines changed

3 files changed

+210
-0
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,29 @@ If you are using another ruby version manager or no manager at all:
5151
$ shopify-ruby resolve 3.2
5252
3.2.2-pshopify4
5353
```
54+
55+
## Creating a new pshopify definition
56+
57+
A rake task automates the creation of new pshopify definition files:
58+
59+
```bash
60+
$ rake pshopify:create # uses latest stable Ruby from ruby-build
61+
$ rake "pshopify:create[4.0.2]" # uses a specific version
62+
```
63+
64+
This will:
65+
- Default to the latest stable CRuby version if none is specified
66+
- Determine the next pshopify number (e.g. `4.0.2-pshopify2` if `4.0.2-pshopify1` already exists)
67+
- Verify the branch exists on [Shopify/ruby](https://github.com/Shopify/ruby)
68+
- Read the openssl line from the upstream ruby-build definition
69+
- Fetch the changelog from the GitHub compare API
70+
- Generate the definition file in `rubies/`
71+
72+
### Prerequisites
73+
74+
- The [`gh` CLI](https://cli.github.com/) must be installed and authenticated
75+
- A local [ruby-build](https://github.com/rbenv/ruby-build) checkout must be available. The task searches for sibling directories matching `*ruby-build`, or you can set `RUBY_BUILD_PATH`:
76+
77+
```bash
78+
$ RUBY_BUILD_PATH=~/src/ruby-build rake "pshopify:create[4.0.2]"
79+
```

Rakefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ require "rubocop/rake_task"
1313

1414
RuboCop::RakeTask.new
1515

16+
Dir.glob("lib/tasks/**/*.rake").each { |r| load r }
17+
1618
task default: [:test, :rubocop]

lib/tasks/pshopify.rake

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# frozen_string_literal: true
2+
3+
require "json"
4+
require "open3"
5+
6+
namespace :pshopify do
7+
desc "Create a new pshopify definition. Usage: rake \"pshopify:create[4.0.2]\" (defaults to latest stable)"
8+
task :create, [:base_version] do |_t, args|
9+
creator = PshopifyDefinitionCreator.new(args[:base_version])
10+
creator.run
11+
end
12+
end
13+
14+
class PshopifyDefinitionCreator
15+
SHOPIFY_RUBY_REPO = "Shopify/ruby"
16+
SHOPIFY_RUBY_GIT_URL = "https://github.com/#{SHOPIFY_RUBY_REPO}.git"
17+
RUBIES_DIR = File.expand_path("../../rubies", __dir__)
18+
19+
def initialize(base_version = nil)
20+
@base_version = base_version
21+
end
22+
23+
def run
24+
@ruby_build_defs_dir = find_ruby_build_defs_dir
25+
@base_version ||= detect_latest_stable
26+
@pshopify_num = next_pshopify_number
27+
@pshopify_version = "#{@base_version}-pshopify#{@pshopify_num}"
28+
@tag = "v#{@pshopify_version}"
29+
30+
puts "Creating definition for #{@pshopify_version}..."
31+
32+
verify_branch_exists
33+
openssl_line = read_upstream_openssl_line
34+
compare_base = determine_compare_base
35+
verify_previous_pshopify_included if @pshopify_num > 1
36+
commits = fetch_changelog(compare_base)
37+
38+
content = generate_definition(compare_base, commits, openssl_line)
39+
output_path = File.join(RUBIES_DIR, @pshopify_version)
40+
File.write(output_path, content)
41+
42+
puts "Created #{output_path}"
43+
end
44+
45+
private
46+
47+
def next_pshopify_number
48+
existing = Dir.children(RUBIES_DIR)
49+
.filter_map { |f| f[/\A#{Regexp.escape(@base_version)}-pshopify(\d+)\z/, 1]&.to_i }
50+
51+
(existing.max || 0) + 1
52+
end
53+
54+
def verify_branch_exists
55+
gh_api("repos/#{SHOPIFY_RUBY_REPO}/branches/#{@tag}")
56+
puts " Branch #{@tag} exists"
57+
rescue GhApiError => e
58+
abort("Error: branch #{@tag} does not exist on #{SHOPIFY_RUBY_REPO}\n#{e.message}")
59+
end
60+
61+
def read_upstream_openssl_line
62+
definition_path = File.join(@ruby_build_defs_dir, @base_version)
63+
64+
unless File.exist?(definition_path)
65+
abort("Error: upstream definition for #{@base_version} not found in #{@ruby_build_defs_dir}")
66+
end
67+
68+
line = File.readlines(definition_path).find { |l| l.include?("openssl") }
69+
abort("Error: no openssl line found in #{definition_path}") unless line
70+
71+
line.chomp
72+
end
73+
74+
def find_ruby_build_defs_dir
75+
candidates = if ENV["RUBY_BUILD_PATH"]
76+
[ENV["RUBY_BUILD_PATH"]]
77+
else
78+
parent = File.dirname(RUBIES_DIR, 2)
79+
Dir.glob(File.join(parent, "*ruby-build"))
80+
end
81+
82+
candidates.each do |dir|
83+
defs_dir = File.join(dir, "share", "ruby-build")
84+
return defs_dir if Dir.exist?(defs_dir)
85+
end
86+
87+
abort("Error: ruby-build definitions not found.\n" \
88+
"Set RUBY_BUILD_PATH to your ruby-build checkout.")
89+
end
90+
91+
def detect_latest_stable
92+
versions = Dir.children(@ruby_build_defs_dir)
93+
.filter_map { |f| f if f.match?(/\A\d+\.\d+\.\d+\z/) }
94+
.sort_by { |v| Gem::Version.new(v) }
95+
96+
abort("Error: no stable Ruby versions found in #{@ruby_build_defs_dir}") if versions.empty?
97+
98+
latest = versions.last
99+
puts "No version specified, using latest stable: #{latest}"
100+
latest
101+
end
102+
103+
def determine_compare_base
104+
if @pshopify_num == 1
105+
resolve_upstream_tag
106+
else
107+
"v#{@base_version}-pshopify#{@pshopify_num - 1}"
108+
end
109+
end
110+
111+
# Upstream tags may use dots (v4.0.2) or underscores (v3_4_8).
112+
def resolve_upstream_tag
113+
tag_with_dots = "v#{@base_version}"
114+
tag_with_underscores = "v#{@base_version.tr(".", "_")}"
115+
116+
[tag_with_dots, tag_with_underscores].each do |tag|
117+
gh_api("repos/#{SHOPIFY_RUBY_REPO}/git/ref/tags/#{tag}")
118+
return tag
119+
rescue GhApiError
120+
next
121+
end
122+
123+
# Fall back to the dotted form for the compare URL even if we can't confirm it
124+
warn("Warning: could not confirm upstream tag format, using #{tag_with_dots}")
125+
tag_with_dots
126+
end
127+
128+
def verify_previous_pshopify_included
129+
prev_tag = "v#{@base_version}-pshopify#{@pshopify_num - 1}"
130+
data = gh_api("repos/#{SHOPIFY_RUBY_REPO}/compare/#{prev_tag}...#{@tag}")
131+
132+
status = data["status"]
133+
if status == "behind" || status == "diverged"
134+
warn("WARNING: #{@tag} is #{status} relative to #{prev_tag}.")
135+
warn(" The new branch may be missing commits from the previous pshopify.")
136+
warn(" Ahead: #{data["ahead_by"]}, Behind: #{data["behind_by"]}")
137+
else
138+
puts " #{@tag} includes all commits from #{prev_tag}"
139+
end
140+
rescue GhApiError => e
141+
warn("Warning: could not compare with previous pshopify: #{e.message}")
142+
end
143+
144+
def fetch_changelog(compare_base)
145+
data = gh_api("repos/#{SHOPIFY_RUBY_REPO}/compare/#{compare_base}...#{@tag}")
146+
commits = data.fetch("commits", [])
147+
commits.map { |c| c.dig("commit", "message").lines.first.chomp }
148+
rescue GhApiError => e
149+
warn("Warning: could not fetch changelog: #{e.message}")
150+
[]
151+
end
152+
153+
def generate_definition(compare_base, commits, openssl_line)
154+
lines = []
155+
lines << "# https://github.com/#{SHOPIFY_RUBY_REPO}/compare/#{compare_base}...Shopify:#{@tag}"
156+
lines << ""
157+
158+
if commits.any?
159+
lines << "# Based off #{compare_base}, with the following changes:"
160+
commits.each { |msg| lines << "# * #{msg}" }
161+
else
162+
lines << "# Based off #{compare_base}"
163+
end
164+
165+
lines << ""
166+
lines << openssl_line
167+
lines << "install_git \"ruby-#{@pshopify_version}\" \"#{SHOPIFY_RUBY_GIT_URL}\" \"#{@tag}\"" \
168+
"autoconf enable_shared standard"
169+
lines << ""
170+
171+
lines.join("\n")
172+
end
173+
174+
class GhApiError < StandardError; end
175+
176+
def gh_api(endpoint)
177+
out, err, status = Open3.capture3("gh", "api", endpoint)
178+
raise GhApiError, "gh api #{endpoint} failed: #{err.strip}" unless status.success?
179+
180+
JSON.parse(out)
181+
end
182+
end

0 commit comments

Comments
 (0)