diff --git a/.github/workflows/auto-merge-bot-prs.yml b/.github/workflows/auto-merge-bot-prs.yml new file mode 100644 index 000000000..42c53360d --- /dev/null +++ b/.github/workflows/auto-merge-bot-prs.yml @@ -0,0 +1,54 @@ +name: Auto-merge bot PRs +on: + workflow_run: + workflows: ["Test this action"] + types: [completed] + +jobs: + check: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.actor.login == 'ruby-builder-bot' && + github.event.workflow_run.pull_requests[0].user.login == 'ruby-builder-bot' && + github.event.workflow_run.pull_requests[0].head.repo.full_name == 'ruby-builder-bot/setup-ruby' + permissions: + contents: read + steps: + - name: Checkout base branch + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Ruby + uses: ruby/setup-ruby@8a836efbcebe5de0fe86b48a775b7a31b5c70c93 + with: + ruby-version: ruby + + - name: Fetch PR head commit + run: | + git fetch --no-tags origin \ + ${{ github.event.workflow_run.pull_requests[0].head.sha }} + + - name: Run automerge checks + run: | + ./automerge-check.rb \ + ${{ github.event.workflow_run.pull_requests[0].base.sha }} \ + ${{ github.event.workflow_run.pull_requests[0].head.sha }} + + merge: + runs-on: ubuntu-latest + needs: check + permissions: + contents: write + pull-requests: write + steps: + - name: Merge PR + env: + GH_TOKEN: ${{ github.token }} + run: | + gh pr merge "${{ github.event.workflow_run.pull_requests[0].number }}" \ + --repo "${{ github.repository }}" \ + --squash \ + --delete-branch diff --git a/automerge-check.rb b/automerge-check.rb new file mode 100755 index 000000000..a7f37d8a1 --- /dev/null +++ b/automerge-check.rb @@ -0,0 +1,91 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "open3" + +ALLOWED_FILES = [ + "README.md", + "dist/index.js", + "ruby-builder-versions.json", + "windows-toolchain-versions.json", + "windows-versions.json", +].freeze + +class AutomergeCheck + attr_reader :errors + + def initialize(base_ref, head_ref = "HEAD") + @base_ref = base_ref + @head_ref = head_ref + @errors = [] + end + + def run + check_changed_files + + if @errors.empty? + puts "All checks passed." + true + else + puts "Automerge check failed:" + @errors.each { |e| puts " - #{e}" } + false + end + end + + def check_changed_files(changed_files = git_changed_files) + disallowed = changed_files - ALLOWED_FILES + + if disallowed.any? + @errors << "Disallowed files changed: #{disallowed.join(', ')}" + end + end + + private + + def git_changed_files + output, status = Open3.capture2("git", "diff", "--name-only", "#{@base_ref}...#{@head_ref}") + unless status.success? + raise "git diff failed: #{output}" + end + output.split("\n").map(&:strip).reject(&:empty?) + end +end + +if __FILE__ == $0 + if ARGV[0] == "--test" + ARGV.clear + require "minitest/autorun" + + class AutomergeCheckTest < Minitest::Test + def setup + @checker = AutomergeCheck.new("master") + end + + def test_allowed_files_not_flagged + @checker.check_changed_files(["README.md", "dist/index.js"]) + assert_empty @checker.errors + end + + def test_disallowed_files_detected + @checker.check_changed_files(["README.md", "evil.js", "dist/index.js"]) + assert_equal 1, @checker.errors.length + assert_match(/evil\.js/, @checker.errors.first) + end + + def test_all_allowed_files_accepted + @checker.check_changed_files(ALLOWED_FILES) + assert_empty @checker.errors + end + end + elsif ARGV.length < 1 || ARGV.length > 2 + puts "Usage: #{$0} [head-ref]" + puts " #{$0} --test" + exit 1 + else + base_ref = ARGV[0] + head_ref = ARGV[1] || "HEAD" + checker = AutomergeCheck.new(base_ref, head_ref) + exit(checker.run ? 0 : 1) + end +end