diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 959d923..d61c13d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,8 +21,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] - + python-version: ${{ github.event_name == 'pull_request' && fromJSON('["3.11", "3.12"]') || fromJSON('["3.10", "3.11", "3.12", "3.13", "3.14"]') }} + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -38,19 +38,24 @@ jobs: uses: astral-sh/setup-uv@v4 with: enable-cache: true + cache-dependency-glob: | + **/pyproject.toml + **/uv.lock - name: Setup dependencies run: make setup - - name: Run unit tests + - name: Run all tests with coverage + run: make test-coverage + + - name: Run unit tests separately (main branch only) + if: github.ref == 'refs/heads/main' run: make test-unit - - name: Run integration tests + - name: Run integration tests separately (main branch only) + if: github.ref == 'refs/heads/main' run: make test-integration - - name: Run all tests with coverage - run: make test-coverage - lint: runs-on: ubuntu-latest steps: @@ -75,7 +80,7 @@ jobs: docker-test: runs-on: ubuntu-latest - needs: [test, lint] + needs: [lint] steps: - name: Clear Space # Note: Manual cleanup sufficient for CPU images (python:3.12-slim base ~150MB) @@ -126,7 +131,7 @@ jobs: docker-test-lb: runs-on: ubuntu-latest - needs: [test, lint] + needs: [lint] steps: - name: Free Disk Space uses: jlumbroso/free-disk-space@v1.3.1 @@ -176,20 +181,16 @@ jobs: docker-test-lb-cpu: runs-on: ubuntu-latest - needs: [test, lint] + needs: [lint] steps: - - name: Free Disk Space - uses: jlumbroso/free-disk-space@v1.3.1 - with: - tool-cache: true - android: true - dotnet: true - haskell: true - large-packages: true - swap-storage: true - - - name: Additional cleanup and report + - name: Clear Space + # Note: Manual cleanup sufficient for CPU images (python:3.12-slim base ~150MB) + # Only CUDA images need aggressive cleanup due to their ~10-15GB size run: | + rm -rf /usr/share/dotnet + rm -rf /opt/ghc + rm -rf "/usr/local/share/boost" + rm -rf "$AGENT_TOOLSDIRECTORY" docker system prune -af df -h @@ -224,9 +225,25 @@ jobs: cache-to: type=gha,mode=max load: true - release: + docker-validation: runs-on: ubuntu-latest needs: [test, lint, docker-test, docker-test-lb, docker-test-lb-cpu] + if: always() + steps: + - name: Check all jobs succeeded + run: | + if [[ "${{ needs.test.result }}" != "success" ]] || \ + [[ "${{ needs.lint.result }}" != "success" ]] || \ + [[ "${{ needs.docker-test.result }}" != "success" ]] || \ + [[ "${{ needs.docker-test-lb.result }}" != "success" ]] || \ + [[ "${{ needs.docker-test-lb-cpu.result }}" != "success" ]]; then + echo "One or more quality checks failed" + exit 1 + fi + + release: + runs-on: ubuntu-latest + needs: [docker-validation] if: github.ref == 'refs/heads/main' outputs: release_created: ${{ steps.release.outputs.release_created }} @@ -244,228 +261,6 @@ jobs: with: token: ${{ steps.app-token.outputs.token }} - docker-main-gpu: - runs-on: ubuntu-latest - needs: [test, lint, docker-test, release] - if: github.ref == 'refs/heads/main' && github.event_name == 'push' && !needs.release.outputs.release_created - steps: - - name: Free Disk Space - uses: jlumbroso/free-disk-space@v1.3.1 - with: - tool-cache: true - android: true - dotnet: true - haskell: true - large-packages: true - swap-storage: true - - - name: Additional cleanup and report - run: | - docker system prune -af - df -h - - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Set up uv - uses: astral-sh/setup-uv@v4 - with: - enable-cache: true - - - name: Setup dependencies - run: uv sync - - - name: Build and push GPU Docker image (main) - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main - cache-from: type=gha - cache-to: type=gha,mode=max - - docker-main-cpu: - runs-on: ubuntu-latest - needs: [test, lint, docker-test, release] - if: github.ref == 'refs/heads/main' && github.event_name == 'push' && !needs.release.outputs.release_created - steps: - - name: Clear Space - run: | - rm -rf /usr/share/dotnet - rm -rf /opt/ghc - rm -rf "/usr/local/share/boost" - rm -rf "$AGENT_TOOLSDIRECTORY" - docker system prune -af - df -h - - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Set up uv - uses: astral-sh/setup-uv@v4 - with: - enable-cache: true - - - name: Setup dependencies - run: uv sync - - - name: Build and push CPU Docker image (main) - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile-cpu - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cpu:main - cache-from: type=gha - cache-to: type=gha,mode=max - - docker-main-lb: - runs-on: ubuntu-latest - needs: [test, lint, docker-test, docker-test-lb, release] - if: github.ref == 'refs/heads/main' && github.event_name == 'push' && !needs.release.outputs.release_created - steps: - - name: Free Disk Space - uses: jlumbroso/free-disk-space@v1.3.1 - with: - tool-cache: true - android: true - dotnet: true - haskell: true - large-packages: true - swap-storage: true - - - name: Additional cleanup and report - run: | - docker system prune -af - df -h - - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Set up uv - uses: astral-sh/setup-uv@v4 - with: - enable-cache: true - - - name: Setup dependencies - run: uv sync - - - name: Build and push Load Balancer Docker image (main) - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile-lb - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-lb:main - cache-from: type=gha - cache-to: type=gha,mode=max - - docker-main-lb-cpu: - runs-on: ubuntu-latest - needs: [test, lint, docker-test, docker-test-lb-cpu, release] - if: github.ref == 'refs/heads/main' && github.event_name == 'push' && !needs.release.outputs.release_created - steps: - - name: Free Disk Space - uses: jlumbroso/free-disk-space@v1.3.1 - with: - tool-cache: true - android: true - dotnet: true - haskell: true - large-packages: true - swap-storage: true - - - name: Additional cleanup and report - run: | - docker system prune -af - df -h - - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Set up uv - uses: astral-sh/setup-uv@v4 - with: - enable-cache: true - - - name: Setup dependencies - run: uv sync - - - name: Build and push CPU Load Balancer Docker image (main) - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile-lb-cpu - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-lb-cpu:main - cache-from: type=gha - cache-to: type=gha,mode=max - docker-prod-gpu: runs-on: ubuntu-latest needs: [release] diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 847028e..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,143 +0,0 @@ -name: Deploy - -on: - workflow_dispatch: - inputs: - tag: - description: 'Docker tag to use' - required: true - default: 'manual' - type: string - skip_tests: - description: 'Skip test suite (emergency deployments only)' - required: false - default: false - type: boolean - -env: - REGISTRY: docker.io - IMAGE_NAME: runpod/flash - -jobs: - test: - runs-on: ubuntu-latest - if: ${{ !inputs.skip_tests }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Set up uv - uses: astral-sh/setup-uv@v4 - with: - enable-cache: true - - - name: Setup dependencies - run: make setup - - - name: Run quality checks - run: make quality-check - - docker: - runs-on: ubuntu-latest - needs: [test] - if: ${{ always() && (success() || inputs.skip_tests) }} - steps: - - name: Clear Space - run: | - rm -rf /usr/share/dotnet - rm -rf /opt/ghc - rm -rf "/usr/local/share/boost" - rm -rf "$AGENT_TOOLSDIRECTORY" - - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Set up uv - uses: astral-sh/setup-uv@v4 - with: - enable-cache: true - - - name: Setup dependencies - run: uv sync - - - name: Build and push GPU Docker image - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ inputs.tag }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Build and push CPU Docker image - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile-cpu - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cpu:${{ inputs.tag }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Build and push Load Balancer Docker image - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile-lb - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-lb:${{ inputs.tag }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Build and push CPU Load Balancer Docker image - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile-lb-cpu - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-lb-cpu:${{ inputs.tag }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Summary - run: | - echo "🚀 **Deployment Complete**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Images pushed:**" >> $GITHUB_STEP_SUMMARY - echo "- \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ inputs.tag }}\`" >> $GITHUB_STEP_SUMMARY - echo "- \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cpu:${{ inputs.tag }}\`" >> $GITHUB_STEP_SUMMARY - echo "- \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-lb:${{ inputs.tag }}\`" >> $GITHUB_STEP_SUMMARY - echo "- \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-lb-cpu:${{ inputs.tag }}\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - if [ "${{ inputs.skip_tests }}" = "true" ]; then - echo "⚠️ **Tests were skipped** (emergency deployment)" >> $GITHUB_STEP_SUMMARY - else - echo "✅ **All tests passed**" >> $GITHUB_STEP_SUMMARY - fi \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index d0bb548..a15c45c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -267,9 +267,9 @@ gpu_config = LiveServerless( - Runs tests and linting on PRs and pushes to main - **Local execution testing**: Automatically tests all `test_*.json` files in src directory to validate handler functionality - Manages releases via `release-please` on main branch - - Builds and pushes `:dev` tagged images on main branch pushes + - Builds and pushes `:main` tagged images on main branch pushes - Builds and pushes production images with semantic versioning on releases -- **Deploy** (`.github/workflows/deploy.yml`): Manual deployment workflow for custom Docker tags and emergency deployments + - Supports manual triggering via `workflow_dispatch` for ad-hoc runs ### Required Secrets Configure these in GitHub repository settings: diff --git a/Makefile b/Makefile index 500c833..3524ac8 100644 --- a/Makefile +++ b/Makefile @@ -131,17 +131,17 @@ build-wip-lb-cpu: setup # Build and push LB CPU image (multi-platform) . --push # Test commands -test: # Run all tests - uv run pytest tests/ -v +test: # Run all tests in parallel + uv run pytest tests/ -v -n auto --dist loadscope test-unit: # Run unit tests only - uv run pytest tests/unit/ -v -m "not integration" + uv run pytest tests/unit/ -v -m "not integration" -n auto --dist loadscope test-integration: # Run integration tests only uv run pytest tests/integration/ -v -m integration -test-coverage: # Run tests with coverage report - uv run pytest tests/ -v --cov=handler --cov=remote_execution --cov-report=term-missing +test-coverage: # Run tests with coverage report (parallel) + uv run pytest tests/ -v -n auto --dist loadscope --cov=handler --cov=remote_execution --cov-report=term-missing test-fast: # Run tests with fast-fail mode uv run pytest tests/ -v -x --tb=short diff --git a/pyproject.toml b/pyproject.toml index 20cae20..c11e140 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dev = [ "pytest-mock>=3.14.0", "pytest-cov>=6.0.0", "pytest-asyncio>=0.24.0", + "pytest-xdist>=3.5.0", "ruff>=0.8.0", "mypy>=1.11.0", "types-requests>=2.25.0", diff --git a/release-please-config.json b/release-please-config.json index 8a32359..beb9adb 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -18,7 +18,5 @@ ] } }, - "bootstrap-sha": "040347b5859ccc7ed230c398721e16d651fae887", - "bump-minor-pre-major": true, - "bump-patch-for-minor-pre-major": true + "bootstrap-sha": "040347b5859ccc7ed230c398721e16d651fae887" } diff --git a/src/handler.py b/src/handler.py index e05df95..5066690 100644 --- a/src/handler.py +++ b/src/handler.py @@ -1,4 +1,3 @@ -import runpod from typing import Dict, Any from runpod_flash.protos.remote_execution import FunctionRequest, FunctionResponse @@ -34,6 +33,8 @@ async def handler(event: Dict[str, Any]) -> Dict[str, Any]: return output.model_dump() # type: ignore[no-any-return] -# Start the RunPod serverless handler +# Start the RunPod serverless handler (only available on RunPod platform) if __name__ == "__main__": + import runpod + runpod.serverless.start({"handler": handler}) diff --git a/src/test-handler.sh b/src/test-handler.sh index 303e3ec..34aa26d 100755 --- a/src/test-handler.sh +++ b/src/test-handler.sh @@ -11,18 +11,18 @@ for test_file in tests/test_*.json; do echo "No test_*.json files found" exit 1 fi - + test_count=$((test_count + 1)) echo "Testing with $test_file..." - - # Run the test and capture output - # In Docker: python is available and has system-installed packages - # Locally: use uv run to manage dependencies - if command -v python &> /dev/null; then + + # Detect if running in Docker + if [ -f /.dockerenv ]; then + # Docker: use system python with pre-installed packages output=$(python handler.py --test_input "$(cat "$test_file")" 2>&1) exit_code=$? else - output=$(uv run python3 handler.py --test_input "$(cat "$test_file")" 2>&1) + # Local: use uv run to manage dependencies + output=$(uv run python handler.py --test_input "$(cat "$test_file")" 2>&1) exit_code=$? fi diff --git a/tests/unit/test_unpack_volume.py b/tests/unit/test_unpack_volume.py index ad14416..b33943b 100644 --- a/tests/unit/test_unpack_volume.py +++ b/tests/unit/test_unpack_volume.py @@ -396,9 +396,10 @@ def test_maybe_unpack_skips_when_should_not_unpack(self, mock_unpack, mock_shoul mock_should_unpack.assert_called_once() mock_unpack.assert_not_called() + @patch("unpack_volume.sleep") @patch("unpack_volume._should_unpack_from_volume") @patch("unpack_volume.unpack_app_from_volume") - def test_maybe_unpack_propagates_exceptions(self, mock_unpack, mock_should_unpack): + def test_maybe_unpack_propagates_exceptions(self, mock_unpack, mock_should_unpack, mock_sleep): """Test that exceptions during unpacking are propagated.""" mock_should_unpack.return_value = True mock_unpack.side_effect = FileNotFoundError("Artifact not found") diff --git a/uv.lock b/uv.lock index 9fcb145..5278c1a 100644 --- a/uv.lock +++ b/uv.lock @@ -925,6 +925,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 }, ] +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708 }, +] + [[package]] name = "fastapi" version = "0.128.0" @@ -2573,6 +2582,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095 }, ] +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396 }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -3687,6 +3709,7 @@ dev = [ { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-mock" }, + { name = "pytest-xdist" }, { name = "rich" }, { name = "ruff" }, { name = "typer" }, @@ -3715,6 +3738,7 @@ dev = [ { name = "pytest-asyncio", specifier = ">=0.24.0" }, { name = "pytest-cov", specifier = ">=6.0.0" }, { name = "pytest-mock", specifier = ">=3.14.0" }, + { name = "pytest-xdist", specifier = ">=3.5.0" }, { name = "rich", specifier = ">=14.0.0" }, { name = "ruff", specifier = ">=0.8.0" }, { name = "typer", specifier = ">=0.12.0" },