This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Smart Test Runner | ||
| # This workflow intelligently determines which tests to run based on file changes | ||
| # Saves CI budget by skipping expensive tests on data-only changes | ||
| on: | ||
| pull_request: | ||
| types: [opened, synchronize, reopened] | ||
| push: | ||
| branches: | ||
| - main | ||
| - develop | ||
| jobs: | ||
| # First job: Analyze what changed to determine test strategy | ||
| analyze-changes: | ||
| name: Analyze Changes | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| run-frontend-tests: ${{ steps.filter.outputs.frontend }} | ||
| run-e2e-tests: ${{ steps.filter.outputs.e2e }} | ||
| run-python-tests: ${{ steps.filter.outputs.python }} | ||
| run-jekyll-build: ${{ steps.filter.outputs.jekyll }} | ||
| is-data-only: ${{ steps.filter.outputs.data-only }} | ||
| test-strategy: ${{ steps.strategy.outputs.strategy }} | ||
| steps: | ||
| - name: 📂 Checkout repository | ||
| uses: actions/checkout@v5 | ||
| with: | ||
| fetch-depth: 0 # Need full history for accurate diff | ||
| - name: 📊 Detect file changes | ||
| uses: dorny/paths-filter@v3 | ||
| id: filter | ||
| with: | ||
| filters: | | ||
| # Frontend JavaScript changes requiring tests | ||
| frontend: | ||
| - 'static/js/**/*.js' | ||
| - '!static/js/**/*.min.js' | ||
| - 'tests/frontend/**' | ||
| - 'package.json' | ||
| - 'package-lock.json' | ||
| - 'jest.config.js' | ||
| # Changes requiring E2E tests | ||
| e2e: | ||
| - 'static/js/**/*.js' | ||
| - '_layouts/**' | ||
| - '_includes/**' | ||
| - '_pages/**' | ||
| - 'tests/e2e/**' | ||
| - 'playwright.config.js' | ||
| # Python utility changes | ||
| python: | ||
| - 'utils/**/*.py' | ||
| - 'tests/python/**' | ||
| - 'pixi.toml' | ||
| - 'pyproject.toml' | ||
| - 'requirements*.txt' | ||
| # Jekyll/Ruby changes | ||
| jekyll: | ||
| - '_config*.yml' | ||
| - '_plugins/**' | ||
| - '_layouts/**' | ||
| - '_includes/**' | ||
| - '_sass/**' | ||
| - 'Gemfile*' | ||
| # Data-only changes (no tests needed) | ||
| data-only: | ||
| - '_data/conferences.yml' | ||
| - '_data/archive.yml' | ||
| - '_data/legacy.yml' | ||
| - '_data/types.yml' | ||
| - '_i18n/**/*.yml' | ||
| - '!**/*.js' | ||
| - '!**/*.py' | ||
| - '!**/*.rb' | ||
| - '!_layouts/**' | ||
| - '!_includes/**' | ||
| - name: 🎯 Determine test strategy | ||
| id: strategy | ||
| run: | | ||
| # Check if this is a data-only change | ||
| if [[ "${{ steps.filter.outputs.data-only }}" == "true" ]] && \ | ||
| [[ "${{ steps.filter.outputs.frontend }}" == "false" ]] && \ | ||
| [[ "${{ steps.filter.outputs.e2e }}" == "false" ]] && \ | ||
| [[ "${{ steps.filter.outputs.python }}" == "false" ]] && \ | ||
| [[ "${{ steps.filter.outputs.jekyll }}" == "false" ]]; then | ||
| echo "strategy=data-validation-only" >> $GITHUB_OUTPUT | ||
| echo "📊 Data-only changes detected - will run minimal validation" | ||
| elif [[ "${{ steps.filter.outputs.frontend }}" == "true" ]] || \ | ||
| [[ "${{ steps.filter.outputs.e2e }}" == "true" ]]; then | ||
| echo "strategy=full-frontend" >> $GITHUB_OUTPUT | ||
| echo "🎭 Frontend changes detected - will run frontend and E2E tests" | ||
| elif [[ "${{ steps.filter.outputs.python }}" == "true" ]]; then | ||
| echo "strategy=python-only" >> $GITHUB_OUTPUT | ||
| echo "🐍 Python changes detected - will run Python tests only" | ||
| elif [[ "${{ steps.filter.outputs.jekyll }}" == "true" ]]; then | ||
| echo "strategy=jekyll-build" >> $GITHUB_OUTPUT | ||
| echo "💎 Jekyll changes detected - will run build test only" | ||
| else | ||
| echo "strategy=minimal" >> $GITHUB_OUTPUT | ||
| echo "✅ Non-critical changes - running minimal checks" | ||
| fi | ||
| - name: 📝 Comment on PR with test plan | ||
| if: github.event_name == 'pull_request' | ||
| uses: actions/github-script@v8 | ||
| with: | ||
| script: | | ||
| const strategy = '${{ steps.strategy.outputs.strategy }}'; | ||
| let comment = '## 🧪 Test Strategy Analysis\n\n'; | ||
| const changes = { | ||
| frontend: ${{ steps.filter.outputs.frontend }}, | ||
| e2e: ${{ steps.filter.outputs.e2e }}, | ||
| python: ${{ steps.filter.outputs.python }}, | ||
| jekyll: ${{ steps.filter.outputs.jekyll }}, | ||
| dataOnly: ${{ steps.filter.outputs.data-only }} | ||
| }; | ||
| comment += '### 📊 Detected Changes:\n'; | ||
| comment += `- Frontend JS: ${changes.frontend ? '✅ Yes' : '❌ No'}\n`; | ||
| comment += `- E2E Required: ${changes.e2e ? '✅ Yes' : '❌ No'}\n`; | ||
| comment += `- Python Utils: ${changes.python ? '✅ Yes' : '❌ No'}\n`; | ||
| comment += `- Jekyll Config: ${changes.jekyll ? '✅ Yes' : '❌ No'}\n`; | ||
| comment += `- Data Only: ${changes.dataOnly ? '✅ Yes' : '❌ No'}\n`; | ||
| comment += '\n'; | ||
| comment += '### 🎯 Test Strategy: `' + strategy + '`\n\n'; | ||
| switch(strategy) { | ||
| case 'data-validation-only': | ||
| comment += '✨ **Optimized Run**: Only conference data validation will run.\n'; | ||
| comment += 'This saves ~15-20 minutes of CI time and reduces costs.\n'; | ||
| break; | ||
| case 'full-frontend': | ||
| comment += '🎭 **Full Frontend Testing**: Running unit tests, E2E tests, and visual regression.\n'; | ||
| break; | ||
| case 'python-only': | ||
| comment += '🐍 **Python Tests Only**: Running Python utility tests and data validation.\n'; | ||
| break; | ||
| case 'jekyll-build': | ||
| comment += '💎 **Jekyll Build Test**: Validating site builds correctly.\n'; | ||
| break; | ||
| default: | ||
| comment += '✅ **Minimal Checks**: Running basic validation only.\n'; | ||
| } | ||
| comment += '\n💰 **Cost Savings**: This intelligent test selection reduces unnecessary CI runs.'; | ||
| // Find and update or create comment | ||
| const { data: comments } = await github.rest.issues.listComments({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.issue.number, | ||
| }); | ||
| const botComment = comments.find(comment => | ||
| comment.user.type === 'Bot' && | ||
| comment.body.includes('Test Strategy Analysis') | ||
| ); | ||
| if (botComment) { | ||
| await github.rest.issues.updateComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| comment_id: botComment.id, | ||
| body: comment | ||
| }); | ||
| } else { | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.issue.number, | ||
| body: comment | ||
| }); | ||
| } | ||
| # Data validation only - for conference data changes | ||
| data-validation: | ||
| name: Data Validation | ||
| needs: analyze-changes | ||
| if: always() # Always run data validation | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: 📂 Checkout repository | ||
| uses: actions/checkout@v5 | ||
| - name: 🐍 Setup Pixi | ||
| uses: prefix-dev/setup-pixi@v0.9.0 | ||
| - name: ✅ Validate conference data | ||
| run: | | ||
| pixi run sort --check | ||
| pixi run validate | ||
| - name: 📊 Check for duplicates | ||
| run: | | ||
| python utils/check_duplicates.py || true | ||
| - name: 🔗 Validate URLs (sample) | ||
| if: needs.analyze-changes.outputs.is-data-only == 'true' | ||
| run: | | ||
| # Only check a sample of URLs for data-only changes to save time | ||
| python utils/check_links.py --sample 10 || true | ||
| # Conditional frontend tests | ||
| frontend-tests: | ||
| name: Frontend Tests | ||
| needs: analyze-changes | ||
| if: needs.analyze-changes.outputs.run-frontend-tests == 'true' | ||
| uses: ./.github/workflows/frontend-tests.yml | ||
|
Check failure on line 220 in .github/workflows/smart-test-runner.yml
|
||
| secrets: inherit | ||
| # Conditional E2E tests | ||
| e2e-tests: | ||
| name: E2E Tests | ||
| needs: analyze-changes | ||
| if: needs.analyze-changes.outputs.run-e2e-tests == 'true' | ||
| uses: ./.github/workflows/e2e-tests.yml | ||
| secrets: inherit | ||
| # Python tests (if utilities changed) | ||
| python-tests: | ||
| name: Python Tests | ||
| needs: analyze-changes | ||
| if: needs.analyze-changes.outputs.run-python-tests == 'true' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: 📂 Checkout repository | ||
| uses: actions/checkout@v5 | ||
| - name: 🐍 Setup Pixi | ||
| uses: prefix-dev/setup-pixi@v0.9.0 | ||
| - name: 🧪 Run Python tests | ||
| run: | | ||
| pixi run test | ||
| # Jekyll build test | ||
| jekyll-build-test: | ||
| name: Jekyll Build Test | ||
| needs: analyze-changes | ||
| if: needs.analyze-changes.outputs.run-jekyll-build == 'true' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: 📂 Checkout repository | ||
| uses: actions/checkout@v5 | ||
| - name: 💎 Setup Ruby | ||
| uses: ruby/setup-ruby@v1 | ||
| with: | ||
| ruby-version: '3.3' | ||
| bundler-cache: true | ||
| - name: 🏗️ Test Jekyll build | ||
| run: | | ||
| bundle exec jekyll build | ||
| env: | ||
| JEKYLL_ENV: test | ||
| # Final status check | ||
| test-status: | ||
| name: Test Status | ||
| runs-on: ubuntu-latest | ||
| needs: [data-validation, frontend-tests, e2e-tests, python-tests, jekyll-build-test] | ||
| if: always() | ||
| steps: | ||
| - name: ✅ All tests passed | ||
| if: | | ||
| needs.data-validation.result == 'success' && | ||
| (needs.frontend-tests.result == 'success' || needs.frontend-tests.result == 'skipped') && | ||
| (needs.e2e-tests.result == 'success' || needs.e2e-tests.result == 'skipped') && | ||
| (needs.python-tests.result == 'success' || needs.python-tests.result == 'skipped') && | ||
| (needs.jekyll-build-test.result == 'success' || needs.jekyll-build-test.result == 'skipped') | ||
| run: | | ||
| echo "✅ All required tests passed!" | ||
| echo "Test strategy successfully reduced unnecessary runs." | ||
| - name: ❌ Some tests failed | ||
| if: | | ||
| needs.data-validation.result == 'failure' || | ||
| needs.frontend-tests.result == 'failure' || | ||
| needs.e2e-tests.result == 'failure' || | ||
| needs.python-tests.result == 'failure' || | ||
| needs.jekyll-build-test.result == 'failure' | ||
| run: | | ||
| echo "❌ Some tests failed. Please check the logs above." | ||
| exit 1 | ||