From dcc7f0502f3e7e41d14b6a4261923ea353fff01b Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Thu, 30 Jul 2026 13:35:17 +0900 Subject: [PATCH] Pick the changelog base from the version being released `gem:changelog` always started from the latest tag. That is right for a prerelease -- `X.Y.Z.pre.N` documents what changed since `X.Y.Z.pre.N-1` -- and wrong for a release proper, where the latest tag is itself a prerelease and the section is supposed to cover the whole cycle, prereleases included. Getting that wrong is quiet: the release comes out with only the tail of the cycle written up. The base now follows `RBS::VERSION`. A prerelease starts from the latest tag; a release proper skips the prerelease tags and starts from the previous release proper. Passing a version explicitly still overrides both. Tags that are not ancestors of the branch being released need no special handling: `git log A..B` already lists what is in B and not in A, which is the same set as from their merge base, and `git describe` only considers reachable tags. A patch release cut from a maintenance branch is therefore skipped on its own. Co-Authored-By: Claude Opus 5 --- Rakefile | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/Rakefile b/Rakefile index 383183c83b..ee78e3fc7a 100644 --- a/Rakefile +++ b/Rakefile @@ -511,9 +511,9 @@ CHANGELOG_SKIP_LABELS = ["skip-changelog"] # Resolves the commit-ish the changelog starts from. # # `version` is a version number, a tag, or any commit-ish. When it is omitted, the latest tag -# matching `tag_glob` is used. +# matching `tag_glob` is used, skipping the ones matching `exclude_globs`. # -def resolve_changelog_base(version, tag_glob:) +def resolve_changelog_base(version, tag_glob:, exclude_globs: []) require "open3" from = @@ -521,7 +521,10 @@ def resolve_changelog_base(version, tag_glob:) # `4.1.0` and `v4.1.0` both mean the tag `v4.1.0`, while `master` or a SHA is used as is. version.match?(/\A\d/) ? "v#{version}" : version else - output, status = Open3.capture2("git", "describe", "--tags", "--match", tag_glob, "--abbrev=0") + command = ["git", "describe", "--tags", "--match", tag_glob, "--abbrev=0"] + exclude_globs.each { |glob| command.push("--exclude", glob) } + + output, status = Open3.capture2(*command) raise "🚨 Cannot detect the latest tag matching `#{tag_glob}`. Give the previous version explicitly." unless status.success? output.chomp end @@ -746,17 +749,33 @@ namespace :gem do # A constant defined in a `namespace` block is a top-level constant, so it needs the prefix. GEM_CHANGELOG_PATHS = [".", ":(exclude)rust"] - desc "Generate changelog template from GH pull requests merged after the given version (defaults to the latest tag)" + # The tags a release proper starts *after*, rather than at. + GEM_PRERELEASE_TAGS = ["v*.pre*", "v*.dev*"] + + # Where the changelog of the release being prepared starts, derived from `RBS::VERSION`: + # + # * `X.Y.Z.pre.N` documents what changed since `X.Y.Z.pre.N-1`, so it starts from the latest tag. + # * `X.Y.Z` documents the whole cycle, the prereleases included, so it skips the prerelease tags + # in between and starts from the previous release proper. + # + # This is the step that is easy to get wrong by hand: on a release proper the latest tag is a + # prerelease, so the obvious default would produce only the tail of the cycle. Passing a version + # explicitly overrides all of it. + # + def changelog_base(version) + excluded = Gem::Version.new(RBS::VERSION).prerelease? ? [] : GEM_PRERELEASE_TAGS + resolve_changelog_base(version, tag_glob: "v*", exclude_globs: excluded) + end + + desc "Generate changelog template from GH pull requests merged since the previous release" task :changelog, [:version] do |_task, args| - from = resolve_changelog_base(args[:version], tag_glob: "v*") - print_changelog(from, paths: GEM_CHANGELOG_PATHS) + print_changelog(changelog_base(args[:version]), paths: GEM_CHANGELOG_PATHS) end namespace :changelog do desc "Print the pull requests of `gem:changelog` as JSON, with the changed files and body of each" task :json, [:version] do |_task, args| - from = resolve_changelog_base(args[:version], tag_glob: "v*") - print_changelog_json(from, paths: GEM_CHANGELOG_PATHS) + print_changelog_json(changelog_base(args[:version]), paths: GEM_CHANGELOG_PATHS) end end end