Skip to content

Reduce unnecessary Modal CI runs on PRs #1316

Reduce unnecessary Modal CI runs on PRs

Reduce unnecessary Modal CI runs on PRs #1316

# 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