diff --git a/Library/Homebrew/brew.sh b/Library/Homebrew/brew.sh index 43b966163f..59c6ec3817 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 bef801d773..50ece18acd 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 0000000000..0863c4ab09 --- /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 dd21cbe17a..4b0f7776ea 100644 --- a/Library/Homebrew/utils/github.rb +++ b/Library/Homebrew/utils/github.rb @@ -482,7 +482,12 @@ module GitHub 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 @@ module GitHub 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 7941e54fee..36d8ddb2f0 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 c63fd6ef0b..c258fbd350 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 361ae2ff3b..b88abaa26f 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 5c90003237..74b80b08fb 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\. .