From 852bdf23b6f871aaafa03b0786c7f0d88dd266f7 Mon Sep 17 00:00:00 2001 From: tomerqodo Date: Thu, 4 Dec 2025 22:40:50 +0200 Subject: [PATCH] Apply changes for benchmark PR --- .../setup-release-environment/action.yml | 43 ++++ .github/workflows/release-handler.yml | 49 ++++ .github/workflows/release-prepare-bump.yml | 39 +++ .../workflows/release-prepare-latest-bump.yml | 29 +++ .github/workflows/tests.yml | 2 +- lib/tasks/release.rake | 232 ++++++++++++++++++ lib/tasks/version_bump.rake | 173 ------------- lib/version.rb | 2 +- spec/models/theme_spec.rb | 2 +- spec/tasks/release_spec.rb | 203 +++++++++++++++ spec/tasks/version_bump_spec.rb | 134 ---------- 11 files changed, 598 insertions(+), 310 deletions(-) create mode 100644 .github/actions/setup-release-environment/action.yml create mode 100644 .github/workflows/release-handler.yml create mode 100644 .github/workflows/release-prepare-bump.yml create mode 100644 .github/workflows/release-prepare-latest-bump.yml create mode 100644 lib/tasks/release.rake create mode 100644 spec/tasks/release_spec.rb diff --git a/.github/actions/setup-release-environment/action.yml b/.github/actions/setup-release-environment/action.yml new file mode 100644 index 0000000000000..7bd6d10e542ac --- /dev/null +++ b/.github/actions/setup-release-environment/action.yml @@ -0,0 +1,43 @@ +name: 'Setup Release Environment' +runs: + using: "composite" + steps: + - name: Install gh cli + shell: bash + run: | + # https://github.com/cli/cli/blob/trunk/docs/install_linux.md#debian-ubuntu-linux-raspberry-pi-os-apt + (type -p wget >/dev/null || (sudo apt update && sudo apt install wget -y)) \ + && sudo mkdir -p -m 755 /etc/apt/keyrings \ + && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + && cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ + && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + && sudo mkdir -p -m 755 /etc/apt/sources.list.d \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ + && sudo apt update \ + && sudo apt install gh -y + + - name: Set working directory owner + shell: bash + run: chown root:root . + + - name: Configure Git + shell: bash + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Bundler cache + uses: actions/cache@v4 + with: + path: vendor/bundle + key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: ${{ runner.os }}-gem- + + - name: Setup gems + shell: bash + run: | + bundle config --local path vendor/bundle + bundle config --local deployment true + bundle config --local without development + bundle install --jobs 4 + bundle clean diff --git a/.github/workflows/release-handler.yml b/.github/workflows/release-handler.yml new file mode 100644 index 0000000000000..fc09a5063737b --- /dev/null +++ b/.github/workflows/release-handler.yml @@ -0,0 +1,49 @@ +name: Release - create tags & branches + +on: + workflow_dispatch: + inputs: + sha: + description: "The commit SHA to prepare the release for" + required: false + type: string + push: + branches: + - main + - release/* + paths: + - "lib/version.rb" + +permissions: + contents: write + pull-requests: write + actions: write + +jobs: + build: + name: run + runs-on: ubuntu-latest + container: discourse/discourse_test:slim + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v6 + with: + filter: tree:0 + fetch-depth: 0 + + - uses: ./.github/actions/setup-release-environment + + - name: Maybe cut branch + id: maybe-cut-branch + if: github.ref == 'refs/heads/main' + run: bin/rake release:maybe_cut_branch[${{ inputs.sha || github.sha }}] + + - name: Maybe trigger prepare next version + if: steps.maybe-cut-branch.outputs.new_branch_name + run: gh workflow run release-prepare-bump.yml -f branch="${{ steps.maybe-cut-branch.outputs.new_branch_name }}" + env: + GH_TOKEN: ${{ github.token }} + + - name: Maybe tag release + run: bin/rake release:maybe_tag_release[${{ inputs.sha || github.sha }}] diff --git a/.github/workflows/release-prepare-bump.yml b/.github/workflows/release-prepare-bump.yml new file mode 100644 index 0000000000000..1bde0ab7bf3f9 --- /dev/null +++ b/.github/workflows/release-prepare-bump.yml @@ -0,0 +1,39 @@ +name: Release - prepare bump on release branch + +on: + workflow_dispatch: + inputs: + branch: + description: "The release branch to prepare the next version for, e.g. `release/2015.11`" + required: true + type: string + push: + branches: + - release/* + paths-ignore: # a version bump on a release branch should not trigger another version bump PR + - 'lib/version.rb' + +permissions: + contents: write + pull-requests: write + +jobs: + build: + name: run + runs-on: ubuntu-latest + container: discourse/discourse_test:slim + timeout-minutes: 10 + + env: + GH_TOKEN: ${{ github.token }} + + steps: + - uses: actions/checkout@v6 + with: + filter: tree:0 + fetch-depth: 0 + + - uses: ./.github/actions/setup-release-environment + + - name: Create a PR for the next version bump + run: bin/rake "release:prepare_next_version_branch[${{ inputs.branch || github.ref_name }}]" diff --git a/.github/workflows/release-prepare-latest-bump.yml b/.github/workflows/release-prepare-latest-bump.yml new file mode 100644 index 0000000000000..ac5802b73e5d8 --- /dev/null +++ b/.github/workflows/release-prepare-latest-bump.yml @@ -0,0 +1,29 @@ +name: Release - prepare bump for main branch + +on: + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + build: + name: run + runs-on: ubuntu-latest + container: discourse/discourse_test:slim + timeout-minutes: 10 + + env: + GH_TOKEN: ${{ github.token }} + + steps: + - uses: actions/checkout@v6 + with: + filter: tree:0 + fetch-depth: 0 + + - uses: ./.github/actions/setup-release-environment + + - name: Create a PR for the next version bump + run: bin/rake release:prepare_next_version diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f5f3248cc3ebe..7be7ae4c83b35 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -82,7 +82,7 @@ jobs: echo "QUNIT_PARALLEL=$(($(nproc) / 2))" >> $GITHUB_ENV fi - - uses: actions/checkout@v6 + - uses: actions/checkout@v5 with: fetch-depth: 1 diff --git a/lib/tasks/release.rake b/lib/tasks/release.rake new file mode 100644 index 0000000000000..99427f1cc3d64 --- /dev/null +++ b/lib/tasks/release.rake @@ -0,0 +1,232 @@ +# frozen_string_literal: true + +module ReleaseUtils + def self.dry_run? + !!ENV["DRY_RUN"] + end + + def self.test_mode? + ENV["RUNNING_RELEASE_IN_RSPEC_TESTS"] == "1" + end + + def self.read_version_rb + File.read("lib/version.rb") + end + + def self.parse_current_version + version = read_version_rb[/STRING = "(.*)"/, 1] + raise "Unable to parse current version" if version.nil? + puts "Parsed current version: #{version.inspect}" + version + end + + def self.write_version(version) + File.write("lib/version.rb", read_version_rb.sub(/STRING = ".*"/, "STRING = \"#{version}\"")) + end + + def self.git(*args, allow_failure: false, silent: false) + puts "> git #{args.inspect}" unless silent + stdout, stderr, status = Open3.capture3({ "LEFTHOOK" => "0" }, "git", *args) + if !status.success? && !allow_failure + raise "Command failed: git #{args.inspect}\n#{stdout.indent(2)}\n#{stderr.indent(2)}" + end + stdout + end + + def self.ref_exists?(ref) + git "rev-parse", "--verify", ref + true + rescue StandardError + false + end + + def self.make_pr(base:, branch:) + return if test_mode? + + args = ["--title", `git log -1 --pretty=%s`.strip, "--body", `git log -1 --pretty=%b`.strip] + + success = + system("gh", "pr", "create", "--base", base, "--head", branch, *args) || + system("gh", "pr", "edit", branch, *args) + + raise "Failed to create or update PR" unless success + end + + def self.with_clean_worktree(origin_branch) + git "fetch", "origin", origin_branch + path = "#{Rails.root}/tmp/version-bump-worktree-#{SecureRandom.hex}" + begin + FileUtils.mkdir_p(path) + git "worktree", "add", path, "origin/#{origin_branch}" + Dir.chdir(path) { yield } # rubocop:disable Discourse/NoChdir + ensure + puts "Cleaning up temporary worktree..." + git "worktree", "remove", "--force", path, silent: true, allow_failure: true + FileUtils.rm_rf(path) + end + end +end + +namespace :release do + desc "Check a commit hash and create a release branch if it's a trigger" + task "maybe_cut_branch", [:check_ref] do |t, args| + check_ref = args[:check_ref] + + ReleaseUtils.with_clean_worktree("main") do + ReleaseUtils.git("checkout", check_ref.to_s) + new_version = ReleaseUtils.parse_current_version + + ReleaseUtils.git("checkout", "#{check_ref}^1") + previous_version = ReleaseUtils.parse_current_version + + next "version has not changed" if new_version == previous_version + + raise "Unexpected previous version" if !previous_version.ends_with? "-latest" + raise "Unexpected new version" if !new_version.ends_with? "-latest" + if Gem::Version.new(new_version) < Gem::Version.new(previous_version) + raise "New version is smaller than old version" + end + + parts = previous_version.split(".") + new_branch_name = "release/#{parts[0]}.#{parts[1]}" + + ReleaseUtils.git("branch", new_branch_name) + puts "Created new branch #{new_branch_name}" + + File.write( + ENV["GITHUB_OUTPUT"] || "/dev/null", + "new_branch_name=#{new_branch_name}\n", + mode: "a", + ) + + if ReleaseUtils.dry_run? + puts "[DRY RUN] Skipping pushing branch #{new_branch_name} to origin" + else + ReleaseUtils.git("push", "--set-upstream", "origin", new_branch_name) + puts "Pushed branch #{new_branch_name} to origin" + end + end + + puts "Done!" + end + + desc "Maybe tag release" + task "maybe_tag_release", [:check_ref] do |t, args| + check_ref = args[:check_ref] + + ReleaseUtils.with_clean_worktree("main") do + ReleaseUtils.git "checkout", check_ref.to_s + release_branches = + ReleaseUtils + .git("branch", "-a", "--contains", check_ref, "release/*", "main") + .lines + .map(&:strip) + if release_branches.empty? + puts "Commit #{check_ref} is not on a release branch. Skipping" + next + end + + current_version = ReleaseUtils.parse_current_version + + tag_name = "v#{current_version}" + + existing_releases = + ReleaseUtils + .git("tag", "-l", "v*") + .lines + .map { |tag| Gem::Version.new(tag.strip.delete_prefix("v")) } + .sort + + if ReleaseUtils.ref_exists?(tag_name) + puts "Tag #{tag_name} already exists, skipping" + else + puts "Tagging release #{tag_name}" + ReleaseUtils.git "tag", "-a", tag_name, "-m", "version #{current_version}" + + if ReleaseUtils.dry_run? + puts "[DRY RUN] Skipping pushing tag to origin" + else + ReleaseUtils.git "push", "origin", "refs/tags/#{tag_name}" + end + + if existing_releases.last && Gem::Version.new(current_version) > existing_releases.last + ReleaseUtils.git "tag", "-a", "release", "-m", "latest release" + if ReleaseUtils.dry_run? + puts "[DRY RUN] Skipping pushing 'release' tag to origin" + else + ReleaseUtils.git "push", "origin", "-f", "refs/tags/release" + end + end + end + end + + puts "Done!" + end + + desc "Prepare a version bump PR for `main`" + task "prepare_next_version" do |t, args| + pr_branch_name = "version-bump/main" + branch = "main" + + ReleaseUtils.with_clean_worktree(branch) do + ReleaseUtils.git "branch", "-D", pr_branch_name if ReleaseUtils.ref_exists?(pr_branch_name) + ReleaseUtils.git "checkout", "-b", pr_branch_name + + current_version = ReleaseUtils.parse_current_version + + target_version_number = "#{Time.now.strftime("%Y.%m")}.0-latest" + + if Gem::Version.new(target_version_number) <= Gem::Version.new(current_version) + puts "Target version #{current_version} is already >= #{target_version_number}. Incrementing instead." + major, minor, patch_and_pre = current_version.split(".") + minor = (minor.to_i + 1).to_s.rjust(2, "0") + target_version_number = "#{major}.#{minor}.#{patch_and_pre}" + end + + ReleaseUtils.write_version(target_version_number) + ReleaseUtils.git "add", "lib/version.rb" + ReleaseUtils.git "commit", + "-m", + "DEV: Begin development of v#{target_version_number}\n\nMerging this will trigger the creation of a `release/#{current_version.sub(".0-latest", "")}` branch on the preceding commit." + ReleaseUtils.git "push", "-f", "--set-upstream", "origin", pr_branch_name + + ReleaseUtils.make_pr(base: branch, branch: pr_branch_name) + end + + puts "Done! Branch #{pr_branch_name} has been pushed to origin and a pull request has been created." + end + + desc "Prepare version bump" + task "prepare_next_version_branch", [:branch] do |t, args| + branch = args[:branch] + + raise "Expected branch to start with 'release/'" if !branch.starts_with?("release/") + + pr_branch_name = "version-bump/#{args[:branch]}" + + ReleaseUtils.with_clean_worktree(branch) do + ReleaseUtils.git "branch", "-D", pr_branch_name if ReleaseUtils.ref_exists?(pr_branch_name) + ReleaseUtils.git "checkout", "-b", pr_branch_name + + current_version = ReleaseUtils.parse_current_version + target_version_number = + if current_version.end_with?("-latest") + current_version.sub("-latest", "") + else + parts = current_version.split(".") + "#{parts[0]}.#{parts[1]}.#{parts[2].to_i + 1}" + end + + ReleaseUtils.write_version(target_version_number) + ReleaseUtils.git "add", "lib/version.rb" + ReleaseUtils.git "commit", + "-m", + "DEV: Bump version on `#{branch}` to `v#{target_version_number}`" + ReleaseUtils.git "push", "-f", "--set-upstream", "origin", pr_branch_name + + ReleaseUtils.make_pr(base: branch, branch: pr_branch_name) + end + + puts "Done! Branch #{pr_branch_name} has been pushed to origin and a pull request has been created." + end +end diff --git a/lib/tasks/version_bump.rake b/lib/tasks/version_bump.rake index 3a6f7b39b869a..b2ea1b44b5288 100644 --- a/lib/tasks/version_bump.rake +++ b/lib/tasks/version_bump.rake @@ -189,179 +189,6 @@ def with_clean_worktree(origin_branch) end end -desc "Stage commits for a beta version bump (e.g. beta1.dev -> beta1 -> beta2.dev). A PR will be created for approval, then the script will prompt to perform the release" -task "version_bump:beta" do - branch = "version_bump/beta" - base = "main" - - with_clean_worktree(base) do - current_version = parse_current_version - - commits = - begin - raise "Expected current version to end in -latest" if !current_version.end_with?("-latest") - - beta_release_version = current_version.sub("-latest", "") - next_dev_version = current_version.sub(/beta(\d+)/) { "beta#{$1.to_i + 1}" } - - [ - PlannedCommit.new( - version: beta_release_version, - tags: [ - PlannedTag.new(name: "beta", message: "latest beta release"), - PlannedTag.new(name: "latest-release", message: "latest release"), - PlannedTag.new( - name: "v#{beta_release_version}", - message: "version #{beta_release_version}", - ), - ], - ), - PlannedCommit.new(version: next_dev_version), - ] - end - - make_commits(commits: commits, branch: branch, base: base) - fastforward(base: base, branch: branch) - stage_tags(commits) - push_tags(commits) - end - - puts "Done!" -end - -desc "Stage commits for minor stable version bump (e.g. 3.1.1 -> 3.1.2). A PR will be created for approval, then the script will prompt to perform the release" -task "version_bump:minor_stable" do - base = "stable" - branch = "version_bump/stable" - - with_clean_worktree(base) do - current_version = parse_current_version - if current_version !~ /^(\d+)\.(\d+)\.(\d+)$/ - raise "Expected current stable version to be in the form X.Y.Z. It was #{current_version}" - end - - new_version = current_version.sub(/\.(\d+)\z/) { ".#{$1.to_i + 1}" } - - commits = [ - PlannedCommit.new( - version: new_version, - tags: [PlannedTag.new(name: "v#{new_version}", message: "version #{new_version}")], - ), - ] - - make_commits(commits: commits, branch: branch, base: base) - fastforward(base: base, branch: branch) - stage_tags(commits) - push_tags(commits) - end - - puts "Done!" -end - -desc "Stage commits for a major version bump (e.g. 3.1.0.beta6-latest -> 3.1.0.beta6 -> 3.1.0 -> 3.2.0.beta1-latest). A PR will be created for approval, then the script will merge to `main`. Should be passed a version number for the next stable version (e.g. 3.2.0)" -task "version_bump:major_stable_prepare", [:next_major_version_number] do |t, args| - unless args[:next_major_version_number] =~ /\A\d+\.\d+\.\d+\z/ - raise "Expected next_major_version number to be in the form X.Y.Z" - end - - base = "main" - branch = "version_bump/beta" - - with_clean_worktree(base) do - current_version = parse_current_version - - raise "Expected current version to end in -latest" if !current_version.end_with?("-latest") - - beta_release_version = current_version.sub("-latest", "") - - next_dev_version = args[:next_major_version_number] + ".beta1-latest" - - final_beta_release = - PlannedCommit.new( - version: beta_release_version, - tags: [ - PlannedTag.new(name: "beta", message: "latest beta release"), - PlannedTag.new(name: "latest-release", message: "latest release"), - PlannedTag.new( - name: "v#{beta_release_version}", - message: "version #{beta_release_version}", - ), - ], - ) - - commits = [final_beta_release, PlannedCommit.new(version: next_dev_version)] - - make_commits(commits: commits, branch: branch, base: base) - fastforward(base: base, branch: branch) - stage_tags(commits) - push_tags(commits) - - puts <<~MSG - The #{base} branch is now ready for a stable release. - Now run this command to merge the release into the stable branch: - bin/rake "version_bump:major_stable_merge[v#{beta_release_version}]" - MSG - end -end - -desc <<~DESC - Stage the merge of a stable version bump into the stable branch. - A PR will be created for approval, then the script will merge to `stable`. - Should be passed the ref of the beta release which should be promoted to stable - (output from the version_bump:major_stable_prepare rake task) - e.g.: - bin/rake "version_bump:major_stable_merge[v3.1.0.beta12]" -DESC -task "version_bump:major_stable_merge", [:version_bump_ref] do |t, args| - merge_ref = args[:version_bump_ref] - raise "Must pass version_bump_ref" if merge_ref.blank? - - git "fetch", "origin", merge_ref - raise "Unknown version_bump_ref: #{merge_ref.inspect}" unless ref_exists?(merge_ref) - - base = "stable" - branch = "version_bump/stable" - - with_clean_worktree(base) do - git "branch", "-D", branch if ref_exists?(branch) - git "checkout", "-b", branch - - git "merge", "--no-commit", merge_ref, allow_failure: true - - out, status = - Open3.capture2e "git diff --binary #{merge_ref} | patch -p1 -R --no-backup-if-mismatch" - raise "Error applying diff\n#{out}}" unless status.success? - - git "add", "." - - merged_version = parse_current_version - git "commit", "-m", "Merge v#{merged_version} into #{base}" - - diff_to_base = git("diff", merge_ref).strip - raise "There are diffs remaining to #{merge_ref}" unless diff_to_base.empty? - - stable_release_version = merged_version.sub(/\.beta\d+\z/, "") - stable_release_commit = - PlannedCommit.new( - version: stable_release_version, - tags: [ - PlannedTag.new( - name: "v#{stable_release_version}", - message: "version #{stable_release_version}", - ), - ], - ) - stable_release_commit.perform! - - git("push", "-f", "--set-upstream", "origin", branch) - - make_pr(base: base, branch: branch, title: "Merge v#{merged_version} into #{base}") - fastforward(base: base, branch: branch) - stage_tags([stable_release_commit]) - push_tags([stable_release_commit]) - end -end - desc <<~DESC squash-merge many security fixes into a single branch for review/merge. Pass a list of comma-separated branches in the SECURITY_FIX_REFS env var, including the remote name. diff --git a/lib/version.rb b/lib/version.rb index 522a3de3e9597..0d4cb6dce6264 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -7,7 +7,7 @@ module Discourse unless defined?(::Discourse::VERSION) module VERSION #:nodoc: # Use the `version_bump:*` rake tasks to update this value - STRING = "3.6.0.beta3-latest" + STRING = "2025.11.0-latest" PARTS = STRING.split(".") private_constant :PARTS diff --git a/spec/models/theme_spec.rb b/spec/models/theme_spec.rb index eb0b17d48638a..c775fb7707cc5 100644 --- a/spec/models/theme_spec.rb +++ b/spec/models/theme_spec.rb @@ -64,7 +64,7 @@ end it "can automatically disable for mismatching version" do - theme.create_remote_theme!(remote_url: "", minimum_discourse_version: "99.99.99") + theme.create_remote_theme!(remote_url: "", minimum_discourse_version: "9999.99.99") theme.save! expect(Theme.transform_ids(theme.id)).to eq([]) diff --git a/spec/tasks/release_spec.rb b/spec/tasks/release_spec.rb new file mode 100644 index 0000000000000..12b49007efdf1 --- /dev/null +++ b/spec/tasks/release_spec.rb @@ -0,0 +1,203 @@ +# frozen_string_literal: true + +def run(*args) + out, err, status = Open3.capture3(*args) + raise "Command failed: #{args.inspect}\n#{out}\n#{err}" unless status.success? + out +end + +def fake_version_rb(version) + File.read("#{Rails.root}/lib/version.rb").sub(/STRING = ".*"/, "STRING = \"#{version}\"") +end + +RSpec.describe "tasks/version_bump" do + let(:tmpdir) { Dir.mktmpdir } + let(:origin_path) { "#{tmpdir}/origin-repo" } + let(:local_path) { "#{tmpdir}/local-repo" } + + before do + ENV["RUNNING_RELEASE_IN_RSPEC_TESTS"] = "1" + + Rake::Task.tasks.each { |t| t.reenable } + FileUtils.mkdir_p origin_path + + Dir.chdir(origin_path) do + FileUtils.mkdir_p "lib" + FileUtils.mkdir_p "tmp" + + File.write(".gitignore", "tmp\n") + File.write("lib/version.rb", fake_version_rb("3.2.0.beta1-latest")) + + run "git", "init" + run "git", "checkout", "-b", "main" + run "git", "add", "." + run "git", "commit", "-m", "Initial commit" + + run "git", "checkout", "-b", "stable" + File.write("#{origin_path}/lib/version.rb", fake_version_rb("3.1.2")) + run "git", "add", "." + run "git", "commit", "-m", "Previous stable version bump" + + run "git", "checkout", "main" + run "git", "config", "receive.denyCurrentBranch", "ignore" + end + + run "git", "clone", "-b", "main", origin_path, local_path + end + + after do + FileUtils.remove_entry(tmpdir) + ENV.delete("RUNNING_VERSION_BUMP_IN_RSPEC_TESTS") + end + + describe "release:maybe_tag_release" do + it "does not tag if commit is not on a release/* branch" do + Dir.chdir(local_path) do + run "git", "switch", "-c", "some-other-branch" + File.write("lib/version.rb", fake_version_rb("2025.06.0")) + run "git", "add", "." + run "git", "commit", "-m", "release 2025.06.0" + commit_hash = run("git", "rev-parse", "HEAD").strip + # Not on a release/* branch, should not tag + capture_stdout { invoke_rake_task("release:maybe_tag_release", commit_hash) } + # Should not tag, so v2025.06.0 should not exist + expect(run("git", "tag").lines.map(&:strip)).not_to include("v2025.06.0") + end + end + + it "tags a release if tag does not exist" do + Dir.chdir(local_path) do + File.write("lib/version.rb", fake_version_rb("2025.03.0")) + run "git", "add", "." + run "git", "commit", "-m", "release 2025.03.0" + commit_hash = run("git", "rev-parse", "HEAD").strip + run "git", "branch", "release/2025.03" + output = capture_stdout { invoke_rake_task("release:maybe_tag_release", commit_hash) } + expect(output).to include("Tagging release v2025.03.0") + expect(run("git", "tag").lines.map(&:strip)).to include("v2025.03.0") + end + end + + it "tags first commit of a pre-release cycle" do + Dir.chdir(local_path) do + File.write("lib/version.rb", fake_version_rb("2025.04.0-latest")) + run "git", "add", "." + run "git", "commit", "-m", "bump to 2025.04.0-latest" + commit_hash = run("git", "rev-parse", "HEAD").strip + output = capture_stdout { invoke_rake_task("release:maybe_tag_release", commit_hash) } + expect(output).to include("Tagging release v2025.04.0-latest") + expect(run("git", "tag").lines.map(&:strip)).to include("v2025.04.0-latest") + end + end + + it "skips tagging if tag already exists" do + Dir.chdir(local_path) do + File.write("lib/version.rb", fake_version_rb("2025.05.0")) + run "git", "add", "." + run "git", "commit", "-m", "release 2025.05.0" + commit_hash = run("git", "rev-parse", "HEAD").strip + run "git", "branch", "release/2025.05" + # Create tag manually + run "git", "tag", "-a", "v2025.05.0", "-m", "version 2025.05.0" + output = capture_stdout { invoke_rake_task("release:maybe_tag_release", commit_hash) } + expect(output).to include("Tag v2025.05.0 already exists, skipping") + end + end + end + + it "can create a new release branch" do + latest_hash, previous_hash = nil + + Dir.chdir(local_path) do + File.write("lib/version.rb", fake_version_rb("2025.01.0-latest")) + run "git", "add", "." + run "git", "commit", "-m", "developing 2025.01" + + previous_hash = run("git", "rev-parse", "HEAD").strip + + File.write("lib/version.rb", fake_version_rb("2025.02.0-latest")) + run "git", "add", "." + run "git", "commit", "-m", "begin development of 2025.02-latest" + + latest_hash = run("git", "rev-parse", "HEAD").strip + + output = capture_stdout { invoke_rake_task("release:maybe_cut_branch", latest_hash) } + expect(output).to include("Created new branch") + end + + Dir.chdir(origin_path) do + run "git", "checkout", "release/2025.01" + branch_tip = run("git", "rev-parse", "HEAD").strip + expect(branch_tip).to eq(previous_hash) + end + end + + describe "release:prepare_next_version" do + it "bumps version to current month format when current version is older" do + Dir.chdir(local_path) do + File.write("lib/version.rb", fake_version_rb("2024.01.0-latest")) + run "git", "add", "." + run "git", "commit", "-m", "old version" + run "git", "push", "origin", "main" + + freeze_time Time.utc(2025, 9, 15) do + capture_stdout { invoke_rake_task("release:prepare_next_version") } + end + end + + Dir.chdir(origin_path) do + run "git", "reset", "--hard" + run "git", "checkout", "version-bump/main" + version_rb_content = File.read("lib/version.rb") + expect(version_rb_content).to include('STRING = "2025.09.0-latest"') + end + end + + it "increments minor version when current version is already >= target month version" do + Dir.chdir(local_path) do + File.write("lib/version.rb", fake_version_rb("2025.10.0-latest")) + run "git", "add", "." + run "git", "commit", "-m", "current month version" + run "git", "push", "origin", "main" + + freeze_time Time.utc(2025, 10, 15) do + output = capture_stdout { invoke_rake_task("release:prepare_next_version") } + expect(output).to include("is already >= 2025.10.0-latest. Incrementing instead.") + end + end + + Dir.chdir(origin_path) do + run "git", "reset", "--hard" + run "git", "checkout", "version-bump/main" + version_rb_content = File.read("lib/version.rb") + expect(version_rb_content).to include('STRING = "2025.11.0-latest"') + end + end + + it "creates version-bump/main branch with proper commit message" do + Dir.chdir(local_path) do + File.write("lib/version.rb", fake_version_rb("2025.05.0-latest")) + run "git", "add", "." + run "git", "commit", "-m", "previous version" + run "git", "push", "origin", "main" + + freeze_time Time.utc(2025, 10, 15) do + capture_stdout { invoke_rake_task("release:prepare_next_version") } + end + end + + Dir.chdir(origin_path) do + run "git", "reset", "--hard" + run "git", "checkout", "version-bump/main" + current_branch = run("git", "branch", "--show-current").strip + expect(current_branch).to eq("version-bump/main") + + commit_message = run("git", "log", "-1", "--pretty=%B").strip + expect(commit_message).to include("Begin development of v2025.10.0-latest") + expect(commit_message).to include( + "Merging this will trigger the creation of a `release/2025.05` branch on the preceding commit.", + ) + end + end + end +end diff --git a/spec/tasks/version_bump_spec.rb b/spec/tasks/version_bump_spec.rb index f4accbf4f5bd6..068375126f5e0 100644 --- a/spec/tasks/version_bump_spec.rb +++ b/spec/tasks/version_bump_spec.rb @@ -52,140 +52,6 @@ def fake_version_rb(version) ENV.delete("RUNNING_VERSION_BUMP_IN_RSPEC_TESTS") end - it "can bump the beta version with version_bump:beta" do - Dir.chdir(local_path) { capture_stdout { invoke_rake_task("version_bump:beta") } } - - Dir.chdir(origin_path) do - # Commits are present with correct messages - expect(run("git", "log", "--pretty=%s").lines.map(&:strip)).to eq( - ["Bump version to v3.2.0.beta2-latest", "Bump version to v3.2.0.beta1", "Initial commit"], - ) - - # Expected tags present - expect(run("git", "tag").lines.map(&:strip)).to contain_exactly( - "latest-release", - "beta", - "v3.2.0.beta1", - ) - - # Tags are all present and attached to the correct commit - %w[latest-release beta v3.2.0.beta1].each do |tag_name| - expect(run("git", "log", "--pretty=%s", "-1", tag_name).strip).to eq( - "Bump version to v3.2.0.beta1", - ) - end - - # Version numbers in version.rb are correct at all commits - expect(run "git", "show", "HEAD", "lib/version.rb").to include( - 'STRING = "3.2.0.beta2-latest"', - ) - expect(run "git", "show", "HEAD~1", "lib/version.rb").to include('STRING = "3.2.0.beta1"') - expect(run "git", "show", "HEAD~2", "lib/version.rb").to include( - 'STRING = "3.2.0.beta1-latest"', - ) - end - end - - it "can perform a minor stable bump with version_bump:minor_stable" do - Dir.chdir(local_path) { capture_stdout { invoke_rake_task("version_bump:minor_stable") } } - - Dir.chdir(origin_path) do - # No commits on main branch - expect(run("git", "log", "--pretty=%s").lines.map(&:strip)).to eq(["Initial commit"]) - - # Expected tags present - expect(run("git", "tag").lines.map(&:strip)).to eq(["v3.1.3"]) - - run "git", "checkout", "stable" - - # Correct commits on stable branch - expect(run("git", "log", "--pretty=%s").lines.map(&:strip)).to eq( - ["Bump version to v3.1.3", "Previous stable version bump", "Initial commit"], - ) - - # Tag points to correct commit - expect(run("git", "log", "--pretty=%s", "-1", "v3.1.3").strip).to eq("Bump version to v3.1.3") - - # Version numbers in version.rb are correct at all commits - expect(run "git", "show", "HEAD", "lib/version.rb").to include('STRING = "3.1.3"') - expect(run "git", "show", "HEAD~1", "lib/version.rb").to include('STRING = "3.1.2"') - end - end - - it "can prepare a major stable bump with version_bump:major_stable_prepare" do - Dir.chdir(local_path) do - capture_stdout { invoke_rake_task("version_bump:major_stable_prepare", "3.3.0") } - end - - Dir.chdir(origin_path) do - # Commits are present with correct messages - expect(run("git", "log", "--pretty=%s").lines.map(&:strip)).to eq( - ["Bump version to v3.3.0.beta1-latest", "Bump version to v3.2.0.beta1", "Initial commit"], - ) - - # Expected tags present - expect(run("git", "tag").lines.map(&:strip)).to contain_exactly( - "latest-release", - "beta", - "v3.2.0.beta1", - ) - - # Tags are all present and attached to the correct commit - %w[latest-release beta v3.2.0.beta1].each do |tag_name| - expect(run("git", "log", "--pretty=%s", "-1", tag_name).strip).to eq( - "Bump version to v3.2.0.beta1", - ) - end - - # Version numbers in version.rb are correct at all commits - expect(run "git", "show", "HEAD:lib/version.rb").to include('STRING = "3.3.0.beta1-latest"') - expect(run "git", "show", "HEAD~1:lib/version.rb").to include('STRING = "3.2.0.beta1"') - expect(run "git", "show", "HEAD~2:lib/version.rb").to include('STRING = "3.2.0.beta1-latest"') - - # No changes to stable branch - expect(run("git", "log", "--pretty=%s", "stable").lines.map(&:strip)).to eq( - ["Previous stable version bump", "Initial commit"], - ) - end - end - - it "can merge a stable release commit into the stable branch with version_bump:major_stable_merge" do - Dir.chdir(local_path) do - # Prepare first, and find sha1 in output - output = capture_stdout { invoke_rake_task("version_bump:major_stable_prepare", "3.3.0") } - stable_bump_commit = output[/major_stable_merge\[(.*)\]/, 1] - capture_stdout { invoke_rake_task("version_bump:major_stable_merge", stable_bump_commit) } - end - - Dir.chdir(origin_path) do - # Commits on stable branch are present with correct messages - expect(run("git", "log", "--pretty=%s", "stable").lines.map(&:strip)).to contain_exactly( - "Merge v3.2.0.beta1 into stable", - "Bump version to v3.2.0", - "Previous stable version bump", - "Bump version to v3.2.0.beta1", - "Initial commit", - ) - - # Most recent commit is the version bump - expect(run("git", "log", "--pretty=%s", "-1", "stable").strip).to eq("Bump version to v3.2.0") - - # Second most recent commit is a merge commit - parents = run("git", "log", "--pretty=%P", "-n", "1", "stable~1").split(/\s+/).map(&:strip) - expect(parents.length).to eq(2) - - # With correct parents - parent_commit_messages = parents.map { |p| run("git", "log", "--pretty=%s", "-1", p).strip } - expect(parent_commit_messages).to contain_exactly( - "Bump version to v3.2.0.beta1", - "Previous stable version bump", - ) - - # Tag is applied to stable version bump commit - expect(run("git", "log", "--pretty=%s", "-1", "v3.2.0").strip).to eq("Bump version to v3.2.0") - end - end - it "can stage a PR of multiple security fixes using version_bump:stage_security_fixes" do Dir.chdir(origin_path) do run "git", "checkout", "-b", "security-fix-one"