-
Notifications
You must be signed in to change notification settings - Fork 259
Automate Hammer CLI release notes integration #2242
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: gh-pages
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -19,47 +19,77 @@ | |||||||||||||||||||
@project_name = ARGV[0] | ||||||||||||||||||||
@current_release_name = ARGV[1] | ||||||||||||||||||||
|
||||||||||||||||||||
# Additional projects to include in release notes | ||||||||||||||||||||
# Both hammer-cli and hammer-cli-foreman versions are stored in the 'hammer-cli' project | ||||||||||||||||||||
ADDITIONAL_PROJECTS = [ | ||||||||||||||||||||
{ project: 'hammer-cli', version_prefix: 'hammer-cli-' }, | ||||||||||||||||||||
{ project: 'hammer-cli', version_prefix: 'hammer-cli-foreman-' } | ||||||||||||||||||||
].freeze | ||||||||||||||||||||
|
||||||||||||||||||||
# Category mappings and transformations | ||||||||||||||||||||
CATEGORY_MAPPINGS = { | ||||||||||||||||||||
# Merge categories - source categories are merged into target category | ||||||||||||||||||||
merge: { | ||||||||||||||||||||
'Smart Proxy' => ['Smart Proxy - Core'], | ||||||||||||||||||||
'SELinux' => ['SELinux - General Foreman'] | ||||||||||||||||||||
}, | ||||||||||||||||||||
|
||||||||||||||||||||
# Rename categories - simple string replacements using regex patterns | ||||||||||||||||||||
rename: { | ||||||||||||||||||||
/^Hammer CLI$/ => 'Hammer CLI', # Keep as-is for base hammer-cli | ||||||||||||||||||||
}, | ||||||||||||||||||||
Comment on lines
+37
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the point of this noop rename? Feels like this can just be dropped. |
||||||||||||||||||||
|
||||||||||||||||||||
# Project-specific category transformations | ||||||||||||||||||||
project_specific: { | ||||||||||||||||||||
'hammer-cli-foreman-' => { | ||||||||||||||||||||
/^Hammer CLI/ => 'Hammer CLI - Foreman' | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
Comment on lines
+42
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This took me a long time to figure out and again, IMHO a bit too complex. |
||||||||||||||||||||
}.freeze | ||||||||||||||||||||
|
||||||||||||||||||||
def url_to_json(url) | ||||||||||||||||||||
uri = URI(url) | ||||||||||||||||||||
response = Net::HTTP.get(uri) | ||||||||||||||||||||
JSON.parse(response) | ||||||||||||||||||||
end | ||||||||||||||||||||
|
||||||||||||||||||||
# Determine version | ||||||||||||||||||||
def get_version_id(project) | ||||||||||||||||||||
def get_version_id(project, release_name = @current_release_name) | ||||||||||||||||||||
url = "#{URL}/projects/#{project}/versions.json" | ||||||||||||||||||||
result = url_to_json(url) | ||||||||||||||||||||
return nil if result['versions'].nil? | ||||||||||||||||||||
result = result['versions'].detect{|v| v['name'] == @current_release_name } | ||||||||||||||||||||
# Find version with matching name (e.g., "3.16.0" or "hammer-cli-3.16.0") | ||||||||||||||||||||
result = result['versions'].detect{|v| v['name'] == release_name } | ||||||||||||||||||||
return nil if result.nil? | ||||||||||||||||||||
result['id'] | ||||||||||||||||||||
rescue StandardError => e | ||||||||||||||||||||
puts "Error getting version list from #{url}: #{e}" | ||||||||||||||||||||
exit 1 | ||||||||||||||||||||
return nil | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should still exit if it fails. It may not be very clean, but it's a simple script and it's good enough error handling.
Suggested change
|
||||||||||||||||||||
end | ||||||||||||||||||||
|
||||||||||||||||||||
@current_release_id = get_version_id(@project_name) | ||||||||||||||||||||
|
||||||||||||||||||||
if @current_release_id.nil? | ||||||||||||||||||||
puts "Release #{@current_release_name} not found in any of the projects #{@project_name}" | ||||||||||||||||||||
puts "Release #{@current_release_name} not found in project #{@project_name}" | ||||||||||||||||||||
exit 1 | ||||||||||||||||||||
end | ||||||||||||||||||||
|
||||||||||||||||||||
def gather_issues(offset = 0) | ||||||||||||||||||||
def gather_issues(project_name, release_id, offset = 0) | ||||||||||||||||||||
params = { | ||||||||||||||||||||
'status_id' => 'closed', | ||||||||||||||||||||
'offset' => offset, | ||||||||||||||||||||
'limit' => 100, | ||||||||||||||||||||
'f[]' => 'cf_12', | ||||||||||||||||||||
'op[cf_12]' => '=', | ||||||||||||||||||||
'v[cf_12][]' => @current_release_id, | ||||||||||||||||||||
'f[]' => 'cf_12', # Filter by custom field 12 (Fix version) | ||||||||||||||||||||
'op[cf_12]' => '=', # Operator: equals | ||||||||||||||||||||
'v[cf_12][]' => release_id, # Value: the release ID we want | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can repeat this parameter to gather the issues for multiple releases in a single API call. I think you can just change |
||||||||||||||||||||
} | ||||||||||||||||||||
url = "#{URL}/issues.json?#{URI.encode_www_form(params)}" | ||||||||||||||||||||
result = url_to_json(url) | ||||||||||||||||||||
# Recursively fetch all pages of results (Redmine paginates at 100 items) | ||||||||||||||||||||
if result['total_count'].to_i - offset - 100 <= 0 | ||||||||||||||||||||
result['issues'] | ||||||||||||||||||||
else | ||||||||||||||||||||
result['issues'] += gather_issues(offset + 100) | ||||||||||||||||||||
result['issues'] += gather_issues(project_name, release_id, offset + 100) | ||||||||||||||||||||
end | ||||||||||||||||||||
end | ||||||||||||||||||||
|
||||||||||||||||||||
|
@@ -68,24 +98,87 @@ def gather_issues(offset = 0) | |||||||||||||||||||
|
||||||||||||||||||||
puts "\n----------[Start of notes]----------\n\n" | ||||||||||||||||||||
|
||||||||||||||||||||
puts "### Release notes for #{@current_release_name}" | ||||||||||||||||||||
def apply_category_mappings(grouped_issues, version_prefix = nil) | ||||||||||||||||||||
# Step 1: Handle merge mappings | ||||||||||||||||||||
CATEGORY_MAPPINGS[:merge].each do |target_category, source_categories| | ||||||||||||||||||||
grouped_issues[target_category] ||= [] # Initialize if doesn't exist | ||||||||||||||||||||
source_categories.each do |source_category| | ||||||||||||||||||||
if grouped_issues.key?(source_category) | ||||||||||||||||||||
# Move issues from source to target category and remove source | ||||||||||||||||||||
grouped_issues[target_category] += grouped_issues.delete(source_category) | ||||||||||||||||||||
end | ||||||||||||||||||||
end | ||||||||||||||||||||
end | ||||||||||||||||||||
Comment on lines
+103
to
+111
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels way overengineered. The previous code was easier to understand IMHO. Now you have ~15 lines of code for what used to be 2 lines. |
||||||||||||||||||||
|
||||||||||||||||||||
# Step 2: Apply project-specific transformations | ||||||||||||||||||||
if version_prefix && CATEGORY_MAPPINGS[:project_specific][version_prefix] | ||||||||||||||||||||
new_grouped_issues = {} | ||||||||||||||||||||
transformations = CATEGORY_MAPPINGS[:project_specific][version_prefix] | ||||||||||||||||||||
|
||||||||||||||||||||
grouped_issues.each do |category, category_issues| | ||||||||||||||||||||
new_category = category | ||||||||||||||||||||
# Apply each regex pattern transformation for this project | ||||||||||||||||||||
transformations.each do |pattern, replacement| | ||||||||||||||||||||
new_category = new_category.gsub(pattern, replacement) | ||||||||||||||||||||
end | ||||||||||||||||||||
new_grouped_issues[new_category] = category_issues | ||||||||||||||||||||
end | ||||||||||||||||||||
grouped_issues = new_grouped_issues | ||||||||||||||||||||
end | ||||||||||||||||||||
|
||||||||||||||||||||
# Step 3: Apply general rename mappings | ||||||||||||||||||||
new_grouped_issues = {} | ||||||||||||||||||||
grouped_issues.each do |category, category_issues| | ||||||||||||||||||||
new_category = category | ||||||||||||||||||||
CATEGORY_MAPPINGS[:rename].each do |pattern, replacement| | ||||||||||||||||||||
new_category = new_category.gsub(pattern, replacement) | ||||||||||||||||||||
end | ||||||||||||||||||||
new_grouped_issues[new_category] = category_issues | ||||||||||||||||||||
end | ||||||||||||||||||||
Comment on lines
+129
to
+137
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The whole renaming isn't used so I think this can be dropped
Suggested change
|
||||||||||||||||||||
|
||||||||||||||||||||
new_grouped_issues | ||||||||||||||||||||
end | ||||||||||||||||||||
|
||||||||||||||||||||
def generate_release_notes_for_project(project_name, release_name, version_prefix = nil) | ||||||||||||||||||||
# Build full version name (e.g., "hammer-cli-3.16.0" instead of just "3.16.0") | ||||||||||||||||||||
full_version_name = version_prefix ? "#{version_prefix}#{release_name}" : release_name | ||||||||||||||||||||
release_id = get_version_id(project_name, full_version_name) | ||||||||||||||||||||
return if release_id.nil? | ||||||||||||||||||||
|
||||||||||||||||||||
issues = gather_issues(project_name, release_id) | ||||||||||||||||||||
return if issues.empty? | ||||||||||||||||||||
|
||||||||||||||||||||
# Group issues by "Project Name" or "Project Name - Category" format | ||||||||||||||||||||
grouped_issues = issues.group_by { |issue| "#{issue['project']['name']}#{ ' - ' + issue['category']['name'] if issue.key?('category') }" } | ||||||||||||||||||||
|
||||||||||||||||||||
# Apply configurable category mappings | ||||||||||||||||||||
grouped_issues = apply_category_mappings(grouped_issues, version_prefix) | ||||||||||||||||||||
|
||||||||||||||||||||
grouped_issues = gather_issues.group_by { |issue| "#{issue['project']['name']}#{ ' - ' + issue['category']['name'] if issue.key?('category') }" } | ||||||||||||||||||||
# Sort categories alphabetically | ||||||||||||||||||||
grouped_issues = Hash[ grouped_issues.sort_by { |key, val| key } ] | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this existed before, but this can be done easier in current Ruby (probably couldn't when it was written). The version in foreman-documentation already does this.
Suggested change
|
||||||||||||||||||||
|
||||||||||||||||||||
grouped_issues['Smart Proxy'] ||= [] | ||||||||||||||||||||
grouped_issues['SELinux'] ||= [] | ||||||||||||||||||||
grouped_issues['Smart Proxy'] += grouped_issues.delete('Smart Proxy - Core') if grouped_issues.key?('Smart Proxy - Core') | ||||||||||||||||||||
grouped_issues['SELinux'] += grouped_issues.delete('SELinux - General Foreman') if grouped_issues.key?('SELinux - General Foreman') | ||||||||||||||||||||
grouped_issues = Hash[ grouped_issues.sort_by { |key, val| key } ] | ||||||||||||||||||||
grouped_issues.each do |category, issues| | ||||||||||||||||||||
next if issues.empty? | ||||||||||||||||||||
puts "#### #{category}" | ||||||||||||||||||||
grouped_issues.each do |category, category_issues| | ||||||||||||||||||||
next if category_issues.empty? | ||||||||||||||||||||
puts "#### #{category}" | ||||||||||||||||||||
|
||||||||||||||||||||
issues.each do |issue| | ||||||||||||||||||||
puts "* #{issue['subject'].gsub('`','\\\`').gsub('<','<').gsub('>','>').gsub('*','\\\*')} ([##{issue['id']}](#{URL}/issues/#{issue['id']}))" | ||||||||||||||||||||
category_issues.each do |issue| | ||||||||||||||||||||
# Escape markdown special characters in issue subject | ||||||||||||||||||||
escaped_subject = issue['subject'].gsub('`','\\\`').gsub('<','<').gsub('>','>').gsub('*','\\\*') | ||||||||||||||||||||
puts "* #{escaped_subject} ([##{issue['id']}](#{URL}/issues/#{issue['id']}))" | ||||||||||||||||||||
end | ||||||||||||||||||||
|
||||||||||||||||||||
puts | ||||||||||||||||||||
end | ||||||||||||||||||||
end | ||||||||||||||||||||
|
||||||||||||||||||||
puts "### Release notes for #{@current_release_name}" | ||||||||||||||||||||
|
||||||||||||||||||||
# Generate notes for main project | ||||||||||||||||||||
generate_release_notes_for_project(@project_name, @current_release_name) | ||||||||||||||||||||
|
||||||||||||||||||||
puts | ||||||||||||||||||||
# Generate notes for additional projects (Hammer CLI components) | ||||||||||||||||||||
ADDITIONAL_PROJECTS.each do |project_config| | ||||||||||||||||||||
generate_release_notes_for_project(project_config[:project], @current_release_name, project_config[:version_prefix]) | ||||||||||||||||||||
end | ||||||||||||||||||||
|
||||||||||||||||||||
puts "*A full list of changes in #{@current_release_name} is available via [Redmine](https://projects.theforeman.org/issues?set_filter=1&sort=id%3Adesc&status_id=closed&f[]=cf_12&op[cf_12]=%3D&v[cf_12]=#{@current_release_id})*" | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is no longer true because you also need to include the Hammer releases. |
||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is too complex. Why don't you simplify this to various separate constants?