From be02591cb6b476375eab4cf675231d550eadfd1c Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Wed, 20 Jan 2021 01:26:12 -0500 Subject: [PATCH 1/4] Add brew release command --- Library/Homebrew/brew.sh | 2 +- Library/Homebrew/cli/args.rbi | 6 ++ Library/Homebrew/dev-cmd/release.rb | 94 ++++++++++++++++++++++++++ Library/Homebrew/utils/github.rb | 8 ++- completions/bash/brew | 18 +++++ completions/internal_commands_list.txt | 1 + docs/Manpage.md | 16 +++++ manpages/brew.1 | 17 +++++ 8 files changed, 160 insertions(+), 2 deletions(-) create mode 100755 Library/Homebrew/dev-cmd/release.rb diff --git a/Library/Homebrew/brew.sh b/Library/Homebrew/brew.sh index 43b966163ff2b..59c6ec3817043 100644 --- a/Library/Homebrew/brew.sh +++ b/Library/Homebrew/brew.sh @@ -172,7 +172,7 @@ update-preinstall() { if [[ "$HOMEBREW_COMMAND" = "install" || "$HOMEBREW_COMMAND" = "upgrade" || "$HOMEBREW_COMMAND" = "bump-formula-pr" || "$HOMEBREW_COMMAND" = "bump-cask-pr" || - "$HOMEBREW_COMMAND" = "bundle" || + "$HOMEBREW_COMMAND" = "bundle" || "$HOMEBREW_COMMAND" = "release" || "$HOMEBREW_COMMAND" = "tap" && $HOMEBREW_ARG_COUNT -gt 1 || "$HOMEBREW_CASK_COMMAND" = "install" || "$HOMEBREW_CASK_COMMAND" = "upgrade" ]] then diff --git a/Library/Homebrew/cli/args.rbi b/Library/Homebrew/cli/args.rbi index bef801d7730cd..50ece18acda54 100644 --- a/Library/Homebrew/cli/args.rbi +++ b/Library/Homebrew/cli/args.rbi @@ -138,6 +138,12 @@ module Homebrew sig { returns(T.nilable(T::Boolean)) } def reset_cache?; end + sig { returns(T.nilable(T::Boolean)) } + def major?; end + + sig { returns(T.nilable(T::Boolean)) } + def minor?; end + sig { returns(T.nilable(String)) } def tag; end diff --git a/Library/Homebrew/dev-cmd/release.rb b/Library/Homebrew/dev-cmd/release.rb new file mode 100755 index 0000000000000..0863c4ab09f85 --- /dev/null +++ b/Library/Homebrew/dev-cmd/release.rb @@ -0,0 +1,94 @@ +# typed: true +# frozen_string_literal: true + +require "cli/parser" + +module Homebrew + extend T::Sig + + module_function + + sig { returns(CLI::Parser) } + def release_args + Homebrew::CLI::Parser.new do + description <<~EOS + Create a new draft Homebrew/brew release with the appropriate version number and release notes. + + By default, `brew release` will bump the patch version number. Pass + `--major` or `--minor` to bump the major or minor version numbers, respectively. + The command will fail if the previous major or minor release was made less than + one month ago. + + Requires write access to the Homebrew/brew repository. + EOS + switch "--major", + description: "Create a major release." + switch "--minor", + description: "Create a minor release." + conflicts "--major", "--minor" + + named_args :none + end + end + + def release + args = release_args.parse + + safe_system "git", "-C", HOMEBREW_REPOSITORY, "fetch", "origin" if Homebrew::EnvConfig.no_auto_update? + + begin + latest_release = GitHub.get_latest_release "Homebrew", "brew" + rescue GitHub::HTTPNotFoundError + odie "No existing releases found!" + end + latest_version = Version.new latest_release["tag_name"] + + if args.major? || args.minor? + one_month_ago = Date.today << 1 + latest_major_minor_release = begin + GitHub.get_release "Homebrew", "brew", "#{latest_version.major_minor}.0" + rescue GitHub::HTTPNotFoundError + nil + end + + if latest_major_minor_release.blank? + opoo "Unable to determine the release date of the latest major/minor release." + elsif Date.parse(latest_major_minor_release["published_at"]) > one_month_ago + odie "The latest major/minor release was less than one month ago." + end + end + + new_version = if args.major? + Version.new [latest_version.major.to_i + 1, 0, 0].join(".") + elsif args.minor? + Version.new [latest_version.major, latest_version.minor.to_i + 1, 0].join(".") + else + Version.new [latest_version.major, latest_version.minor, latest_version.patch.to_i + 1].join(".") + end.to_s + + ohai "Creating draft release for version #{new_version}" + release_notes = if args.major? || args.minor? + ["Release notes for this release can be found on the [Homebrew blog](https://brew.sh/blog/#{new_version})."] + else + [] + end + release_notes += Utils.popen_read( + "git", "-C", HOMEBREW_REPOSITORY, "log", "--pretty=format:'%s >> - %b%n'", "#{latest_version}..origin/HEAD" + ).lines.grep(/Merge pull request/).map! do |s| + pr = s.gsub(%r{.*Merge pull request #(\d+) from ([^/]+)/[^>]*(>>)*}, + "https://github.com/Homebrew/brew/pull/\\1 (@\\2)") + /(.*\d)+ \(@(.+)\) - (.*)/ =~ pr + "- [#{Regexp.last_match(3)}](#{Regexp.last_match(1)}) (@#{Regexp.last_match(2)})" + end + + begin + release = GitHub.create_or_update_release "Homebrew", "brew", new_version, + body: release_notes.join("\n"), draft: true + rescue *GitHub::API_ERRORS => e + odie "Unable to create release: #{e.message}!" + end + + puts release["html_url"] + exec_browser release["html_url"] + end +end diff --git a/Library/Homebrew/utils/github.rb b/Library/Homebrew/utils/github.rb index dd21cbe17a83b..4b0f7776ea3b5 100644 --- a/Library/Homebrew/utils/github.rb +++ b/Library/Homebrew/utils/github.rb @@ -482,7 +482,12 @@ def get_release(user, repo, tag) open_api(url, request_method: :GET) end - def create_or_update_release(user, repo, tag, id: nil, name: nil, draft: false) + def get_latest_release(user, repo) + url = "#{API_URL}/repos/#{user}/#{repo}/releases/latest" + open_api(url, request_method: :GET) + end + + def create_or_update_release(user, repo, tag, id: nil, name: nil, body: nil, draft: false) url = "#{API_URL}/repos/#{user}/#{repo}/releases" method = if id url += "/#{id}" @@ -495,6 +500,7 @@ def create_or_update_release(user, repo, tag, id: nil, name: nil, draft: false) name: name || tag, draft: draft, } + data[:body] = body if body.present? open_api(url, data: data, request_method: method, scopes: CREATE_ISSUE_FORK_OR_PR_SCOPES) end diff --git a/completions/bash/brew b/completions/bash/brew index 7941e54feefc3..36d8ddb2f03fc 100644 --- a/completions/bash/brew +++ b/completions/bash/brew @@ -1663,6 +1663,23 @@ _brew_reinstall() { __brew_complete_casks } +_brew_release() { + local cur="${COMP_WORDS[COMP_CWORD]}" + case "$cur" in + -*) + __brewcomp " + --debug + --help + --major + --minor + --quiet + --verbose + " + return + ;; + esac +} + _brew_release_notes() { local cur="${COMP_WORDS[COMP_CWORD]}" case "$cur" in @@ -2418,6 +2435,7 @@ _brew() { prof) _brew_prof ;; readall) _brew_readall ;; reinstall) _brew_reinstall ;; + release) _brew_release ;; release-notes) _brew_release_notes ;; remove) _brew_remove ;; rm) _brew_rm ;; diff --git a/completions/internal_commands_list.txt b/completions/internal_commands_list.txt index c63fd6ef0bcaf..c258fbd350802 100644 --- a/completions/internal_commands_list.txt +++ b/completions/internal_commands_list.txt @@ -72,6 +72,7 @@ pr-upload prof readall reinstall +release release-notes remove rm diff --git a/docs/Manpage.md b/docs/Manpage.md index 361ae2ff3b75d..b88abaa26f317 100644 --- a/docs/Manpage.md +++ b/docs/Manpage.md @@ -1208,6 +1208,22 @@ Run Homebrew with a Ruby profiler, e.g. `brew prof readall`. * `--stackprof`: Use `stackprof` instead of `ruby-prof` (the default). +### `release` [*`--major`*] [*`--minor`*] + +Create a new draft Homebrew/brew release with the appropriate version number and release notes. + +By default, `brew release` will bump the patch version number. Pass +`--major` or `--minor` to bump the major or minor version numbers, respectively. +The command will fail if the previous major or minor release was made less than +one month ago. + +Requires write access to the Homebrew/brew repository. + +* `--major`: + Create a major release. +* `--minor`: + Create a minor release. + ### `release-notes` [*`options`*] [*`previous_tag`*] [*`end_ref`*] Print the merged pull requests on Homebrew/brew between two Git refs. diff --git a/manpages/brew.1 b/manpages/brew.1 index 5c9000323765d..74b80b08fb69a 100644 --- a/manpages/brew.1 +++ b/manpages/brew.1 @@ -1686,6 +1686,23 @@ Run Homebrew with a Ruby profiler, e\.g\. \fBbrew prof readall\fR\. \fB\-\-stackprof\fR Use \fBstackprof\fR instead of \fBruby\-prof\fR (the default)\. . +.SS "\fBrelease\fR [\fI\-\-major\fR] [\fI\-\-minor\fR]" +Create a new draft Homebrew/brew release with the appropriate version number and release notes\. +. +.P +By default, \fBbrew release\fR will bump the patch version number\. Pass \fB\-\-major\fR or \fB\-\-minor\fR to bump the major or minor version numbers, respectively\. The command will fail if the previous major or minor release was made less than one month ago\. +. +.P +Requires write access to the Homebrew/brew repository\. +. +.TP +\fB\-\-major\fR +Create a major release\. +. +.TP +\fB\-\-minor\fR +Create a minor release\. +. .SS "\fBrelease\-notes\fR [\fIoptions\fR] [\fIprevious_tag\fR] [\fIend_ref\fR]" Print the merged pull requests on Homebrew/brew between two Git refs\. If no \fIprevious_tag\fR is provided it defaults to the latest tag\. If no \fIend_ref\fR is provided it defaults to \fBorigin/master\fR\. . From e1f73e407ad5e5ac4dcb602625d930235ed49a53 Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Thu, 21 Jan 2021 17:43:52 -0500 Subject: [PATCH 2/4] Add ReleaseNotes module --- Library/Homebrew/dev-cmd/release-notes.rb | 21 ++------------ Library/Homebrew/dev-cmd/release.rb | 24 ++++++---------- Library/Homebrew/release_notes.rb | 35 +++++++++++++++++++++++ 3 files changed, 47 insertions(+), 33 deletions(-) create mode 100644 Library/Homebrew/release_notes.rb diff --git a/Library/Homebrew/dev-cmd/release-notes.rb b/Library/Homebrew/dev-cmd/release-notes.rb index 53df5261934b6..b77b2654564ec 100644 --- a/Library/Homebrew/dev-cmd/release-notes.rb +++ b/Library/Homebrew/dev-cmd/release-notes.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "cli/parser" +require "release_notes" module Homebrew extend T::Sig @@ -55,25 +56,9 @@ def release_notes odie "Ref #{ref} does not exist!" end - output = Utils.popen_read( - "git", "-C", HOMEBREW_REPOSITORY, "log", "--pretty=format:'%s >> - %b%n'", "#{previous_tag}..#{end_ref}" - ).lines.grep(/Merge pull request/) - - output.map! do |s| - s.gsub(%r{.*Merge pull request #(\d+) from ([^/]+)/[^>]*(>>)*}, - "https://github.com/Homebrew/brew/pull/\\1 (@\\2)") - end - if args.markdown? - output.map! do |s| - /(.*\d)+ \(@(.+)\) - (.*)/ =~ s - "- [#{Regexp.last_match(3)}](#{Regexp.last_match(1)}) (@#{Regexp.last_match(2)})" - end - end + release_notes = ReleaseNotes.generate_release_notes previous_tag, end_ref, markdown: T.must(args.markdown?) $stderr.puts "Release notes between #{previous_tag} and #{end_ref}:" - if args.markdown? && args.named.first - puts "Release notes for major and minor releases can be found in the [Homebrew blog](https://brew.sh/blog/)." - end - puts output + puts release_notes end end diff --git a/Library/Homebrew/dev-cmd/release.rb b/Library/Homebrew/dev-cmd/release.rb index 0863c4ab09f85..a2834742812eb 100755 --- a/Library/Homebrew/dev-cmd/release.rb +++ b/Library/Homebrew/dev-cmd/release.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "cli/parser" +require "release_notes" module Homebrew extend T::Sig @@ -59,31 +60,24 @@ def release end new_version = if args.major? - Version.new [latest_version.major.to_i + 1, 0, 0].join(".") + Version.new "#{latest_version.major.to_i + 1}.0.0" elsif args.minor? - Version.new [latest_version.major, latest_version.minor.to_i + 1, 0].join(".") + Version.new "#{latest_version.major}.#{latest_version.minor.to_i + 1}.0" else - Version.new [latest_version.major, latest_version.minor, latest_version.patch.to_i + 1].join(".") + Version.new "#{latest_version.major}.#{latest_version.minor}.#{latest_version.patch.to_i + 1}" end.to_s ohai "Creating draft release for version #{new_version}" + release_notes = if args.major? || args.minor? - ["Release notes for this release can be found on the [Homebrew blog](https://brew.sh/blog/#{new_version})."] + "Release notes for this release can be found on the [Homebrew blog](https://brew.sh/blog/#{new_version}).\n" else - [] - end - release_notes += Utils.popen_read( - "git", "-C", HOMEBREW_REPOSITORY, "log", "--pretty=format:'%s >> - %b%n'", "#{latest_version}..origin/HEAD" - ).lines.grep(/Merge pull request/).map! do |s| - pr = s.gsub(%r{.*Merge pull request #(\d+) from ([^/]+)/[^>]*(>>)*}, - "https://github.com/Homebrew/brew/pull/\\1 (@\\2)") - /(.*\d)+ \(@(.+)\) - (.*)/ =~ pr - "- [#{Regexp.last_match(3)}](#{Regexp.last_match(1)}) (@#{Regexp.last_match(2)})" + "" end + release_notes += ReleaseNotes.generate_release_notes latest_version, "origin/HEAD", markdown: true begin - release = GitHub.create_or_update_release "Homebrew", "brew", new_version, - body: release_notes.join("\n"), draft: true + release = GitHub.create_or_update_release "Homebrew", "brew", new_version, body: release_notes, draft: true rescue *GitHub::API_ERRORS => e odie "Unable to create release: #{e.message}!" end diff --git a/Library/Homebrew/release_notes.rb b/Library/Homebrew/release_notes.rb new file mode 100644 index 0000000000000..9ae0d6fc4ab9d --- /dev/null +++ b/Library/Homebrew/release_notes.rb @@ -0,0 +1,35 @@ +# typed: true +# frozen_string_literal: true + +# Helper functions for generating release notes. +# +# @api private +module ReleaseNotes + extend T::Sig + + module_function + + sig { + params(start_ref: T.any(String, Version), end_ref: T.any(String, Version), markdown: T::Boolean) + .returns(String) + } + def generate_release_notes(start_ref, end_ref, markdown: false) + log_output = Utils.popen_read( + "git", "-C", HOMEBREW_REPOSITORY, "log", "--pretty=format:'%s >> - %b%n'", "#{start_ref}..#{end_ref}" + ).lines.grep(/Merge pull request/) + + log_output.map! do |s| + s.gsub(%r{.*Merge pull request #(\d+) from ([^/]+)/[^>]*(>>)*}, + "https://github.com/Homebrew/brew/pull/\\1 (@\\2)") + end + + if markdown + log_output.map! do |s| + /(.*\d)+ \(@(.+)\) - (.*)/ =~ s + "- [#{Regexp.last_match(3)}](#{Regexp.last_match(1)}) (@#{Regexp.last_match(2)})\n" + end + end + + log_output.join + end +end From f382f380342ea395ee0823032eed2791fbc8bef7 Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Thu, 21 Jan 2021 18:25:45 -0500 Subject: [PATCH 3/4] docs: update release documentation --- docs/Releases.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/Releases.md b/docs/Releases.md index e4274414693da..48a300b27b743 100644 --- a/docs/Releases.md +++ b/docs/Releases.md @@ -11,16 +11,12 @@ Homebrew release: [Homebrew/discussions (forum)](https://github.com/homebrew/discussions/discussions) to see if there is anything pressing that needs to be fixed or merged before the next release. If so, fix and merge these changes. -2. After no code changes have happened for at least a couple of hours (ideally 24 hours) - and you are confident there's no major regressions on the current `master` - branch you can create a new Git tag. Ideally this should be signed with your - GPG key. This can then be pushed to GitHub. -3. Use `brew release-notes --markdown $PREVIOUS_TAG` to generate the release - notes for the release. -4. [Create a new release on GitHub](https://github.com/Homebrew/brew/releases/new) - based on the new tag. - -You can watch a video of the above process [on YouTube](https://youtu.be/dQCpLaXOf6k) +2. Ensure that no code changes have happened for at least a couple of hours (ideally 24 hours) + and that you are confident there are no major regressions on the current `master` + branch. +3. Run `brew release` to create a new draft release. For major or minor version bumps, + pass `--major` or `--minor`, respectively. +4. Publish the draft release on [GitHub](https://github.com/Homebrew/brew/releases). If this is a major or minor release (e.g. X.0.0 or X.Y.0) then there are a few more steps: From e13dc902df8fdabfde7f50c1a2a3d772e77d6c5e Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Sat, 23 Jan 2021 02:06:12 -0500 Subject: [PATCH 4/4] Add tests and fix type signature --- Library/Homebrew/dev-cmd/release-notes.rb | 5 ++- Library/Homebrew/release_notes.rb | 2 +- Library/Homebrew/test/dev-cmd/release_spec.rb | 8 +++++ Library/Homebrew/test/release_notes_spec.rb | 33 +++++++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 Library/Homebrew/test/dev-cmd/release_spec.rb create mode 100644 Library/Homebrew/test/release_notes_spec.rb diff --git a/Library/Homebrew/dev-cmd/release-notes.rb b/Library/Homebrew/dev-cmd/release-notes.rb index b77b2654564ec..cfa8b5479a336 100644 --- a/Library/Homebrew/dev-cmd/release-notes.rb +++ b/Library/Homebrew/dev-cmd/release-notes.rb @@ -32,6 +32,9 @@ def release_notes_args def release_notes args = release_notes_args.parse + # TODO: (2.8) Deprecate this command now that the `brew release` command exists. + # odeprecated "`brew release-notes`" + previous_tag = args.named.first if previous_tag.present? @@ -56,7 +59,7 @@ def release_notes odie "Ref #{ref} does not exist!" end - release_notes = ReleaseNotes.generate_release_notes previous_tag, end_ref, markdown: T.must(args.markdown?) + release_notes = ReleaseNotes.generate_release_notes previous_tag, end_ref, markdown: args.markdown? $stderr.puts "Release notes between #{previous_tag} and #{end_ref}:" puts release_notes diff --git a/Library/Homebrew/release_notes.rb b/Library/Homebrew/release_notes.rb index 9ae0d6fc4ab9d..19cdba9a17560 100644 --- a/Library/Homebrew/release_notes.rb +++ b/Library/Homebrew/release_notes.rb @@ -10,7 +10,7 @@ module ReleaseNotes module_function sig { - params(start_ref: T.any(String, Version), end_ref: T.any(String, Version), markdown: T::Boolean) + params(start_ref: T.any(String, Version), end_ref: T.any(String, Version), markdown: T.nilable(T::Boolean)) .returns(String) } def generate_release_notes(start_ref, end_ref, markdown: false) diff --git a/Library/Homebrew/test/dev-cmd/release_spec.rb b/Library/Homebrew/test/dev-cmd/release_spec.rb new file mode 100644 index 0000000000000..509e9a220e2b0 --- /dev/null +++ b/Library/Homebrew/test/dev-cmd/release_spec.rb @@ -0,0 +1,8 @@ +# typed: false +# frozen_string_literal: true + +require "cmd/shared_examples/args_parse" + +describe "Homebrew.release_args" do + it_behaves_like "parseable arguments" +end diff --git a/Library/Homebrew/test/release_notes_spec.rb b/Library/Homebrew/test/release_notes_spec.rb new file mode 100644 index 0000000000000..a65e347b0a52c --- /dev/null +++ b/Library/Homebrew/test/release_notes_spec.rb @@ -0,0 +1,33 @@ +# typed: false +# frozen_string_literal: true + +require "release_notes" + +describe ReleaseNotes do + before do + HOMEBREW_REPOSITORY.cd do + system "git", "init" + system "git", "commit", "--allow-empty", "-m", "Initial commit" + system "git", "tag", "release-notes-testing" + system "git", "commit", "--allow-empty", "-m", "Merge pull request #1 from Homebrew/fix", "-m", "Do something" + system "git", "commit", "--allow-empty", "-m", "make a change" + system "git", "commit", "--allow-empty", "-m", "Merge pull request #2 from User/fix", "-m", "Do something else" + end + end + + describe ".generate_release_notes" do + it "generates release notes" do + expect(described_class.generate_release_notes("release-notes-testing", "HEAD")).to eq <<~NOTES + https://github.com/Homebrew/brew/pull/2 (@User) - Do something else + https://github.com/Homebrew/brew/pull/1 (@Homebrew) - Do something + NOTES + end + + it "generates markdown release notes" do + expect(described_class.generate_release_notes("release-notes-testing", "HEAD", markdown: true)).to eq <<~NOTES + - [Do something else](https://github.com/Homebrew/brew/pull/2) (@User) + - [Do something](https://github.com/Homebrew/brew/pull/1) (@Homebrew) + NOTES + end + end +end