Skip to content

Commit

Permalink
Merge pull request #3835 from rubygems/rewrite_changelog_management
Browse files Browse the repository at this point in the history
Rewrite changelog management in ruby

(cherry picked from commit 0871be1)
  • Loading branch information
deivid-rodriguez committed Oct 5, 2020
1 parent 272c308 commit 6138662
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 160 deletions.
56 changes: 0 additions & 56 deletions .github/release-drafter-bundler.yml

This file was deleted.

49 changes: 9 additions & 40 deletions .github/workflows/changelog-bundler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,55 +20,24 @@ jobs:
ruby-version: 2.7.1
bundler: none

- name: Check for changelog modifications
id: path_check
run: echo "::set-output name=changelog_modified::$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -c '^bundler/CHANGELOG.md$')"

- name: Install dependencies
run: bin/rake dev:deps
working-directory: ./bundler
if: steps.path_check.outputs.changelog_modified != '0'

- name: Update release draft from changelog
run: bin/rake release:github_draft
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
working-directory: ./bundler
if: steps.path_check.outputs.changelog_modified != '0'

- name: Get target version
id: get_target_version
run: echo "::set-output name=target_version::$(bin/rake release:target_version)"
working-directory: ./bundler
if: steps.path_check.outputs.changelog_modified == '0'

- name: Publish changelog draft
id: publish_github_release_draft
uses: release-drafter/release-drafter@v5
with:
config-name: release-drafter-bundler.yml
name: bundler-v${{ steps.get_target_version.outputs.target_version }}
tag: bundler-v${{ steps.get_target_version.outputs.target_version }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
if: steps.path_check.outputs.changelog_modified == '0'

- name: Write new changelog
run: bin/rake release:write_changelog
env:
NEW_CHANGELOG_CONTENT: ${{ steps.publish_github_release_draft.outputs.body }}
- name: Sync changelog
run: bin/rake release:sync_changelog
working-directory: ./bundler
if: steps.path_check.outputs.changelog_modified == '0'

- name: Set up git config
run: |
git config user.name "The Bundler Bot"
git config user.email "bot@bundler.io"
if: steps.path_check.outputs.changelog_modified == '0'
- name: Commit and show the changes
- name: Commit and push changes
run: |
git add bundler/CHANGELOG.md
git commit -m "Automatic changelog update"
git push origin master
if: steps.path_check.outputs.changelog_modified == '0'
if [ "$(git diff)" != "" ]
then
git add bundler/CHANGELOG.md
git commit -m "Automatic changelog update"
git push origin master
fi
12 changes: 4 additions & 8 deletions bundler/doc/playbooks/MERGING_A_PR.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
Bundler requires all CI status checks to pass before a PR can me merged. So make
sure that's the case before merging.

Also, bundler manages the changelog and github release drafts automatically
using information from merged PRs. So, if a PR has user visible changes that
should be included in a future release, make sure the following information is
accurate:
Also, bundler manages the changelog automatically using information from merged
PRs. So, if a PR has user visible changes that should be included in a future
release, make sure the following information is accurate:

* The PR has a good descriptive title. That will be the wording for the
corresponding changelog entry.
Expand All @@ -24,10 +23,7 @@ accurate:
* "bundler: minor enhancement"
* "bundler: bug fix"

This label will indicate the section in the changelog that the PR will take,
and will also define the target version for the next release. For example, if
you merge a PR tagged as "type: breaking change", the next target version used
for the github release draft will be a major version.
This label will indicate the section in the changelog that the PR will take.

Finally, don't forget to review the changes in detail. Make sure you try them
locally if they are not trivial and make sure you request changes and ask as
Expand Down
167 changes: 111 additions & 56 deletions bundler/task/release.rake
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,101 @@ namespace :release do
join_and_strip(lines[current_version_index..previous_version_index])
end

def replace_unreleased_notes(new_content)
new_content_with_references = new_content.gsub(/#(\d+)/, '[#\1](https://github.com/rubygems/rubygems/pull/\1)')
def sync
lines = []

group_by_labels(pull_requests_since_last_release).each do |label, pulls|
category = changelog_label_mapping[label]

lines << "## #{category}"
lines << ""

pulls.sort_by(&:merged_at).reverse_each do |pull|
lines << " - #{pull.title} [##{pull.number}](#{pull.html_url})"
end

lines << ""
end

replace_unreleased_notes(lines)
end

private

def group_by_labels(pulls)
grouped_pulls = pulls.group_by do |pull|
relevant_label_for(pull)
end

grouped_pulls.delete(nil) # exclude non categorized pulls

full_new_changelog = [unreleased_section_title, "", new_content_with_references, released_notes].join("\n") + "\n"
grouped_pulls.sort do |a, b|
changelog_labels.index(a[0]) <=> changelog_labels.index(b[0])
end.to_h
end

def pull_requests_since_last_release
last_release_date = gh_client.releases("rubygems/rubygems").select {|release| !release.draft && release.tag_name =~ /^bundler-v/ }.sort_by(&:created_at).last.created_at

pr_ids = merged_pr_ids_since(last_release_date)

pull_requests_for(pr_ids)
end

def changelog_label_mapping
{
"bundler: security fix" => "Security fixes:",
"bundler: breaking change" => "Breaking changes:",
"bundler: major enhancement" => "Major enhancements:",
"bundler: deprecation" => "Deprecations:",
"bundler: feature" => "Features:",
"bundler: performance" => "Performance:",
"bundler: documentation" => "Documentation:",
"bundler: minor enhancement" => "Minor enhancements:",
"bundler: bug fix" => "Bug fixes:",
}
end

def replace_unreleased_notes(new_content)
full_new_changelog = [unreleased_section_title, "", new_content, released_notes].join("\n") + "\n"

File.open("CHANGELOG.md", "w:UTF-8") {|f| f.write(full_new_changelog) }
end

def unreleased_notes
join_and_strip(lines.take_while {|line| !line.start_with?(release_section_token) })
def relevant_label_for(pull)
relevant_labels = pull.labels.map(&:name) & changelog_labels
return unless relevant_labels.any?

raise "#{pull.html_url} has multiple labels that map to changelog sections" unless relevant_labels.size == 1

relevant_labels.first
end

private
def changelog_labels
changelog_label_mapping.keys
end

def merged_pr_ids_since(date)
commits = `git log --oneline origin/master --since '#{date}'`.split("\n").map {|l| l.split(/\s/, 2) }
commits.map do |_sha, message|
match = /Merge pull request #(\d+)/.match(message)
next unless match

match[1].to_i
end.compact
end

def pull_requests_for(ids)
pulls = gh_client.pull_requests("rubygems/rubygems", :sort => :updated, :state => :closed, :direction => :desc)

loop do
pulls.select! {|pull| ids.include?(pull.number) }

return pulls if (pulls.map(&:number) & ids).to_set == ids.to_set

pulls.concat gh_client.get(gh_client.last_response.rels[:next].href)
end
end

def unreleased_section_title
"#{release_section_token}(Unreleased)"
Expand All @@ -79,18 +161,24 @@ namespace :release do
def release_section_token
"# "
end

def gh_client
GithubInfo.client
end
end

def new_gh_client
token = ENV["GITHUB_TOKEN"]
module GithubInfo
extend self

unless token
require "netrc"
_username, token = Netrc.read["api.github.com"]
end
def client
@client ||= begin
require "netrc"
_username, token = Netrc.read["api.github.com"]

require "octokit"
Octokit::Client.new(:access_token => token)
require "octokit"
Octokit::Client.new(:access_token => token)
end
end
end

desc "Push the release to Github releases"
Expand All @@ -99,47 +187,14 @@ namespace :release do
release_notes = Changelog.new.release_notes(version)
tag = "bundler-v#{version}"

new_gh_client.create_release "rubygems/rubygems", tag, :name => tag,
:body => release_notes,
:prerelease => version.prerelease?
end

desc "Prints the current version in the version file, which should be the next release target"
task :target_version do
print Bundler::GemHelper.gemspec.version
end

desc "Sync the current draft release with the changelog"
task :github_draft do
version = Bundler::GemHelper.gemspec.version
unreleased_notes = Changelog.new.unreleased_notes
tag = "bundler-v#{version}"

gh_client = new_gh_client
gh_client.auto_paginate = true

releases = gh_client.releases("rubygems/rubygems")
release_draft = releases.find {|release| release.draft == true }

options = {
:body => unreleased_notes,
:prerelease => version.prerelease?,
:draft => true,
}

if release_draft
gh_client.update_release(release_draft.url, options.merge(:tag_name => tag))
else
gh_client.create_release("rubygems/rubygems", tag, options)
end
GithubInfo.client.create_release "rubygems/rubygems", tag, :name => tag,
:body => release_notes,
:prerelease => version.prerelease?
end

desc "Replace the unreleased section in the changelog with new content. Pass the new content through ENV['NEW_CHANGELOG_CONTENT']"
task :write_changelog do
new_content = ENV["NEW_CHANGELOG_CONTENT"]
raise "You need to pass some content to write through ENV['NEW_CHANGELOG_CONTENT']" unless new_content

Changelog.new.replace_unreleased_notes(new_content)
desc "Replace the unreleased section in the changelog with up to date content according to merged PRs since the last release"
task :sync_changelog do
Changelog.new.sync
end

desc "Prepare a patch release with the PRs from master in the patch milestone"
Expand All @@ -159,7 +214,7 @@ namespace :release do

puts "Cherry-picking PRs milestoned for #{version} (currently #{current_version}) into the stable branch..."

gh_client = new_gh_client
gh_client = GithubInfo.client

milestones = gh_client.milestones("rubygems/rubygems", :state => "open")

Expand All @@ -177,8 +232,8 @@ namespace :release do
branch = Gem::Version.new(version).segments.map.with_index {|s, i| i == 0 ? s + 1 : s }[0, 2].join(".")
sh("git", "checkout", "-b", "release_bundler/#{version}", branch)

commits = `git log --oneline origin/master -- bundler`.split("\n").map {|l| l.split(/\s/, 2) }.reverse
commits.select! {|_sha, message| message =~ /(Auto merge of|Merge pull request|Merge) ##{Regexp.union(*prs)}/ }
commits = `git log --oneline origin/master`.split("\n").map {|l| l.split(/\s/, 2) }.reverse
commits.select! {|_sha, message| message =~ /Merge pull request ##{Regexp.union(*prs)}/ }

abort "Could not find commits for all PRs" unless commits.size == prs.size

Expand Down

0 comments on commit 6138662

Please sign in to comment.