Reduce unnecessary Modal CI runs on PRs #1316
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
| # Workflow that runs on code changes to a pull request. | |
| name: PR code changes | |
| on: | |
| pull_request: | |
| branches: | |
| - main | |
| paths: | |
| - pyproject.toml | |
| - uv.lock | |
| - modal_app/** | |
| - policyengine_us_data/** | |
| - tests/** | |
| - .github/workflows/** | |
| - Makefile | |
| concurrency: | |
| group: pr-code-changes-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| jobs: | |
| check-fork: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check if PR is from fork | |
| run: | | |
| if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then | |
| echo "❌ ERROR: This PR is from a fork repository." | |
| echo "PRs must be created from branches in the main PolicyEngine/policyengine-us-data repository." | |
| echo "Please close this PR and create a new one following these steps:" | |
| echo "1. git checkout main" | |
| echo "2. git pull upstream main" | |
| echo "3. git checkout -b your-branch-name" | |
| echo "4. git push -u upstream your-branch-name" | |
| echo "5. Create PR from the upstream branch" | |
| exit 1 | |
| fi | |
| echo "✅ PR is from the correct repository" | |
| decide-test-scope: | |
| name: Decide PR test scope | |
| runs-on: ubuntu-latest | |
| needs: check-fork | |
| outputs: | |
| full_suite: ${{ steps.decide.outputs.full_suite }} | |
| reason: ${{ steps.decide.outputs.reason }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - id: decide | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| PR_LABELS_JSON: ${{ toJson(github.event.pull_request.labels.*.name) }} | |
| run: | | |
| python - <<'PY' | |
| import fnmatch | |
| import json | |
| import os | |
| import subprocess | |
| labels = set(json.loads(os.environ["PR_LABELS_JSON"])) | |
| changed_files = subprocess.check_output( | |
| [ | |
| "git", | |
| "diff", | |
| "--name-only", | |
| os.environ["BASE_SHA"], | |
| os.environ["HEAD_SHA"], | |
| ], | |
| text=True, | |
| ).splitlines() | |
| full_suite_label = "full-data-ci" | |
| critical_patterns = [ | |
| ".github/workflows/pr_code_changes.yaml", | |
| ".github/workflows/reusable_test.yaml", | |
| "modal_app/**", | |
| "policyengine_us_data/calibration/**", | |
| "policyengine_us_data/datasets/**", | |
| "policyengine_us_data/db/**", | |
| "policyengine_us_data/storage/download_private_prerequisites.py", | |
| "policyengine_us_data/utils/loss.py", | |
| "policyengine_us_data/utils/mortgage_interest.py", | |
| "policyengine_us_data/utils/soi.py", | |
| "policyengine_us_data/utils/uprating.py", | |
| ] | |
| matched_files = [ | |
| path | |
| for path in changed_files | |
| if any(fnmatch.fnmatch(path, pattern) for pattern in critical_patterns) | |
| ] | |
| if full_suite_label in labels: | |
| full_suite = True | |
| reason = f"label:{full_suite_label}" | |
| elif matched_files: | |
| full_suite = True | |
| reason = f"critical-path:{matched_files[0]}" | |
| else: | |
| full_suite = False | |
| reason = "basic-pytest-only" | |
| with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: | |
| output.write(f"full_suite={'true' if full_suite else 'false'}\n") | |
| output.write(f"reason={reason}\n") | |
| summary = [ | |
| "### PR test scope", | |
| f"- full suite: `{'true' if full_suite else 'false'}`", | |
| f"- reason: `{reason}`", | |
| ] | |
| if matched_files: | |
| summary.append(f"- first matching file: `{matched_files[0]}`") | |
| with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as out: | |
| out.write("\n".join(summary) + "\n") | |
| PY | |
| check-lock-freshness: | |
| name: Check uv.lock freshness | |
| runs-on: ubuntu-latest | |
| needs: check-fork | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.13' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v5 | |
| - name: Check lock file is up-to-date | |
| run: | | |
| uv lock --locked || { | |
| echo "::error::uv.lock is outdated. Run 'uv lock' and commit the changes." | |
| exit 1 | |
| } | |
| Lint: | |
| needs: [check-fork, check-lock-freshness] | |
| uses: ./.github/workflows/reusable_lint.yaml | |
| SmokeTestForMultipleVersions: | |
| name: Smoke test (${{ matrix.os }}, Python ${{ matrix.python-version }}) | |
| runs-on: ${{ matrix.os }} | |
| needs: [check-fork, Lint] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest] | |
| python-version: ['3.13'] | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v4 | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install package ONLY (no dev deps) | |
| run: python -m pip install . | |
| - name: Test basic import | |
| run: python -c "import policyengine_us_data; print('Minimal import OK')" | |
| - name: Test specific core import | |
| run: python -c "from policyengine_core.data import Dataset; print('Core import OK')" | |
| Test: | |
| needs: [check-fork, Lint, decide-test-scope] | |
| uses: ./.github/workflows/reusable_test.yaml | |
| with: | |
| full_suite: ${{ needs.decide-test-scope.outputs.full_suite == 'true' }} | |
| upload_data: false | |
| deploy_docs: false | |
| secrets: inherit |