diff --git a/.github/scripts/test-changes.sh b/.github/scripts/test-changes.sh new file mode 100644 index 0000000..53bb0c2 --- /dev/null +++ b/.github/scripts/test-changes.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +case "$EVENT_NAME" in + pull_request) + comparison="${BASE_SHA}...${HEAD_SHA}" + ;; + push) + if [[ "$BASE_SHA" == 0000000000000000000000000000000000000000 ]] || + ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then + echo '::notice::No previous push revision available; running the full test matrix.' + echo 'run_tests=true' >> "$GITHUB_OUTPUT" + exit 0 + fi + comparison="${BASE_SHA}..${HEAD_SHA}" + ;; + *) + echo "::error::Unsupported event: $EVENT_NAME" + exit 1 + ;; +esac + +changed_files="$(mktemp)" +trap 'rm -f "$changed_files"' EXIT +git diff --name-only --no-renames -z "$comparison" > "$changed_files" + +run_tests=false +while IFS= read -r -d '' path; do + case "$path" in + *.md|LICENSE|LICENSE.txt|images/*) ;; + *) run_tests=true ;; + esac +done < "$changed_files" + +echo "run_tests=$run_tests" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/test-changes.yml b/.github/workflows/test-changes.yml new file mode 100644 index 0000000..f047575 --- /dev/null +++ b/.github/workflows/test-changes.yml @@ -0,0 +1,25 @@ +--- +name: Test changes +on: + workflow_call: + outputs: + run_tests: + value: ${{ jobs.changes.outputs.run_tests }} +permissions: + contents: read +jobs: + changes: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + run_tests: ${{ steps.changes.outputs.run_tests }} + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - id: changes + env: + EVENT_NAME: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: bash .github/scripts/test-changes.sh diff --git a/.github/workflows/test-mongodb.yml b/.github/workflows/test-mongodb.yml index 796dbf3..661eb41 100644 --- a/.github/workflows/test-mongodb.yml +++ b/.github/workflows/test-mongodb.yml @@ -7,7 +7,10 @@ concurrency: group: mongodb-${{ github.ref }} cancel-in-progress: true jobs: + changes: + uses: ./.github/workflows/test-changes.yml test: + needs: changes runs-on: ubuntu-24.04 timeout-minutes: 20 env: @@ -34,7 +37,7 @@ jobs: name: test (ruby=${{ matrix.entry.ruby }}, mongoid=${{ matrix.entry.mongoid }}, mongodb=${{ matrix.entry.mongodb }}, grape=${{ matrix.entry.grape || 'auto' }}, json=${{ matrix.entry.json }}) services: mongodb: - image: mongo:${{ matrix.entry.mongodb }} + image: ${{ needs.changes.outputs.run_tests == 'true' && format('mongo:{0}', matrix.entry.mongodb) || '' }} ports: - 27017:27017 options: >- @@ -44,13 +47,20 @@ jobs: --health-retries 10 steps: - uses: actions/checkout@v7 + if: needs.changes.outputs.run_tests == 'true' - uses: ruby/setup-ruby@v1 + if: needs.changes.outputs.run_tests == 'true' with: ruby-version: ${{ matrix.entry.ruby }} bundler-cache: true cache-version: mongoid-${{ matrix.entry.mongoid }}-grape-${{ matrix.entry.grape || 'auto' }}-json-${{ matrix.entry.json }} - uses: browser-actions/setup-firefox@v1.7.2 + if: needs.changes.outputs.run_tests == 'true' with: firefox-version: latest - name: Test + if: needs.changes.outputs.run_tests == 'true' run: xvfb-run --auto-servernum bundle exec rake spec + - name: Documentation only + if: needs.changes.outputs.run_tests == 'false' + run: echo 'Documentation-only change; database, dependencies, browser, and tests skipped.' diff --git a/.github/workflows/test-postgresql.yml b/.github/workflows/test-postgresql.yml index fbaa6b8..727b085 100644 --- a/.github/workflows/test-postgresql.yml +++ b/.github/workflows/test-postgresql.yml @@ -7,7 +7,10 @@ concurrency: group: postgresql-${{ github.ref }} cancel-in-progress: true jobs: + changes: + uses: ./.github/workflows/test-changes.yml test: + needs: changes runs-on: ubuntu-24.04 timeout-minutes: 20 env: @@ -35,7 +38,7 @@ jobs: name: test (ruby=${{ matrix.entry.ruby }}, postgresql=${{ matrix.entry.postgresql }}, active_record=${{ matrix.entry.active_record }}, grape=${{ matrix.entry.grape || 'auto' }}, json=${{ matrix.entry.json }}) services: postgresql: - image: postgres:${{ matrix.entry.postgresql }} + image: ${{ needs.changes.outputs.run_tests == 'true' && format('postgres:{0}', matrix.entry.postgresql) || '' }} env: POSTGRES_DB: slack_ruby_bot_server_test POSTGRES_USER: test @@ -49,13 +52,20 @@ jobs: --health-retries 10 steps: - uses: actions/checkout@v7 + if: needs.changes.outputs.run_tests == 'true' - uses: ruby/setup-ruby@v1 + if: needs.changes.outputs.run_tests == 'true' with: ruby-version: ${{ matrix.entry.ruby }} bundler-cache: true cache-version: activerecord-${{ matrix.entry.active_record }}-grape-${{ matrix.entry.grape || 'auto' }}-json-${{ matrix.entry.json }} - uses: browser-actions/setup-firefox@v1.7.2 + if: needs.changes.outputs.run_tests == 'true' with: firefox-version: latest - name: Test + if: needs.changes.outputs.run_tests == 'true' run: xvfb-run --auto-servernum bundle exec rake spec + - name: Documentation only + if: needs.changes.outputs.run_tests == 'false' + run: echo 'Documentation-only change; database, dependencies, browser, and tests skipped.' diff --git a/CHANGELOG.md b/CHANGELOG.md index 9595ec4..c195175 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### 2.3.0 (Next) * [#186](https://github.com/slack-ruby/slack-ruby-bot-server/pull/186): Document persisted custom Team attributes for both adapters and Rails concern loading/reloading - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent). +* [#187](https://github.com/slack-ruby/slack-ruby-bot-server/pull/187): Skip expensive database test setup for documentation-only changes while preserving every matrix check and Danger - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent). * [#183](https://github.com/slack-ruby/slack-ruby-bot-server/pull/183): Add a `config.server_pages` block to configure page middleware options centrally without changing the installed library's defaults - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent). * [#184](https://github.com/slack-ruby/slack-ruby-bot-server/pull/184): Add a configurable API application and separately mountable team read/registration endpoints, preserving the default API while supporting custom routes and registration-only applications - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent). * [#185](https://github.com/slack-ruby/slack-ruby-bot-server/pull/185): Skip inactive installations in status checks and deactivate revoked credentials through service callbacks, returning an explicit failed ping instead of failing the whole status response - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b49c768..eb33665 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,6 +47,8 @@ On Linux, install [Firefox](https://www.mozilla.org/firefox/) and `Xvfb`, then r xvfb-run --auto-servernum bundle exec rake ``` +Documentation-only changes (`*.md`, `LICENSE`, `LICENSE.txt`, and `images/`) do not start database containers, install dependencies or Firefox, or run the database test suites. The workflows still publish every existing matrix check name so required checks can complete; Danger and RuboCop remain unchanged. Code, assets in `public/`, dependency files, and CI configuration still run the full matrices. New branches or unavailable previous push revisions conservatively run all tests. + ## Write Tests Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. diff --git a/spec/ci/test_changes_spec.rb b/spec/ci/test_changes_spec.rb new file mode 100644 index 0000000..e34fe27 --- /dev/null +++ b/spec/ci/test_changes_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' +require 'open3' +require 'tmpdir' +require 'fileutils' + +describe 'CI test change classification' do + around do |example| + Dir.mktmpdir('test-changes') do |directory| + @repository = directory + example.run + end + end + + before do + git('init', '--quiet') + git('config', 'user.name', 'CI') + git('config', 'user.email', 'ci@example.invalid') + @base = commit('README.md') + end + + def git(*args) + output, error, status = Open3.capture3('git', *args, chdir: @repository) + raise error unless status.success? + + output.strip + end + + def commit(path) + FileUtils.mkdir_p(File.dirname(File.join(@repository, path))) + File.write(File.join(@repository, path), "content for #{path}\n") + git('add', '--all') + git('commit', '--quiet', '-m', 'Test change') + git('rev-parse', 'HEAD') + end + + def classify(base: @base, head: git('rev-parse', 'HEAD'), event: 'pull_request') + output = File.join(@repository, 'output') + script = File.expand_path('../../.github/scripts/test-changes.sh', __dir__) + _, error, status = Open3.capture3( + { 'EVENT_NAME' => event, 'BASE_SHA' => base, 'HEAD_SHA' => head, 'GITHUB_OUTPUT' => output }, + 'bash', script, chdir: @repository + ) + raise error unless status.success? + + File.read(output).strip + end + + it 'skips Markdown and documentation images' do + commit('docs/guide.md') + commit('images/example.png') + expect(classify).to eq 'run_tests=false' + end + + it 'runs tests when documentation is mixed with runtime changes' do + commit('UPGRADING.md') + commit('public/styles.css') + expect(classify).to eq 'run_tests=true' + end + + it 'does not ignore a code file renamed to Markdown' do + @base = commit('lib/example.rb') + File.rename(File.join(@repository, 'lib/example.rb'), File.join(@repository, 'example.md')) + git('add', '--all') + git('commit', '--quiet', '-m', 'Rename') + expect(classify).to eq 'run_tests=true' + end + + it 'compares pull requests from their merge base, not unrelated base-branch changes' do + head = commit('CHANGELOG.md') + git('checkout', '--quiet', '--detach', @base) + base = commit('lib/base-only.rb') + expect(classify(base: base, head: head)).to eq 'run_tests=false' + end + + it 'includes removed code when a push rewrites history' do + before = commit('lib/removed.rb') + git('checkout', '--quiet', '--detach', @base) + head = commit('CHANGELOG.md') + expect(classify(base: before, head: head, event: 'push')).to eq 'run_tests=true' + end + + it 'runs the full matrix for a new branch' do + expect(classify(base: '0' * 40, event: 'push')).to eq 'run_tests=true' + end + + it 'runs the full matrix when a previous pushed revision is unavailable' do + expect(classify(base: 'f' * 40, event: 'push')).to eq 'run_tests=true' + end + + it 'fails instead of silently skipping tests when a pull request comparison is invalid' do + expect { classify(base: 'f' * 40) }.to raise_error(RuntimeError) + end +end