diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 000000000..22e92a405 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,64 @@ +name: Prepare release + +on: + workflow_dispatch: + inputs: + bump: + description: Version bump type + required: true + type: choice + options: + - patch + - minor + - major + +jobs: + prepare: + name: Bump version and open release PR + runs-on: ubuntu-latest + permissions: + contents: write # push release branch + pull-requests: write # open draft PR + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 # required to reach tag history + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + + - name: Bump version + id: version + run: | + gem install gem-release --no-document + gem bump --version ${{ inputs.bump }} --no-commit + NEW=$(ruby -e "require_relative 'lib/factory_bot/version'; puts FactoryBot::VERSION") + echo "new=$NEW" >> "$GITHUB_OUTPUT" + + - name: Install dependencies + run: bundle install + + - name: Update NEWS.md + env: + GH_TOKEN: ${{ github.token }} + run: bundle exec rake "release:update_changelog[${{ steps.version.outputs.new }}]" + + - name: Push branch and open draft PR + env: + GH_TOKEN: ${{ github.token }} + NEW_VERSION: ${{ steps.version.outputs.new }} + run: | + BRANCH="release/v$NEW_VERSION" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git add lib/factory_bot/version.rb NEWS.md + git commit -m "Release v$NEW_VERSION" + git push origin "$BRANCH" + gh pr create \ + --draft \ + --base main \ + --head "$BRANCH" \ + --title "Release v$NEW_VERSION" \ + --body "Bumps version to \`$NEW_VERSION\` and updates the changelog with merged PRs since the last release." diff --git a/Rakefile b/Rakefile index a5077b70f..f885eaf9d 100644 --- a/Rakefile +++ b/Rakefile @@ -8,6 +8,8 @@ require "standard/rake" Bundler::GemHelper.install_tasks(name: "factory_bot") +Dir.glob("tasks/*.rake").each { |f| load f } + desc "Default: run all specs and standard" task default: %w[all_specs standard] diff --git a/tasks/release.rake b/tasks/release.rake new file mode 100644 index 000000000..cf8cae1ae --- /dev/null +++ b/tasks/release.rake @@ -0,0 +1,39 @@ +require "date" +require "json" + +namespace :release do + desc "Update NEWS.md with merged PRs since the last release tag. Usage: rake release:update_changelog[VERSION]" + task :update_changelog, [:version] do |_, args| + version = args.fetch(:version) { abort "Usage: rake release:update_changelog[VERSION]" } + + # Find the last release tag and when it was made + last_tag = `git tag --sort=-version:refname`.split.grep(/^v?[0-9]/).first + since_date = `git log -1 --format=%as #{last_tag}`.strip + + # Collect merged PRs since that date via the GitHub CLI + raw = `gh pr list --state merged --base main --search "merged:>#{since_date}" --json number,title,author,url --limit 200` + prs = JSON.parse(raw) + pr_entries = prs + .map { |pr| "* #{pr["title"]} by @#{pr.dig("author", "login")} in [##{pr["number"]}](#{pr["url"]})" } + .join("\n") + + news = File.read("NEWS.md") + today = Date.today.strftime("%B %-d, %Y") + + # Extract and remove the Unreleased section (if present) + unreleased = "" + if news =~ /^## Unreleased\n(.*?)(?=^## )/m + unreleased = $1.strip + news = news.sub(/^## Unreleased\n.*?(?=^## )/m, "") + end + + # Combine unreleased entries and merged PR entries + body = [unreleased, pr_entries].reject(&:empty?).join("\n") + new_section = "## #{version} (#{today})\n\n#{body}\n" + + news.sub!(/^(# News\n+)/, "\\1#{new_section}\n") + File.write("NEWS.md", news) + + puts "Updated NEWS.md for v#{version}" + end +end