Skip test_key_file_permissions on Windows #49
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: Terradev CI (Professional) | ||
| on: | ||
| push: | ||
| branches: [ main, develop ] | ||
| pull_request: | ||
| branches: [ main, develop ] | ||
| workflow_dispatch: | ||
| inputs: | ||
| run_integration: | ||
| description: 'Run live integration tests' | ||
| required: false | ||
| default: false | ||
| type: boolean | ||
| test_provider: | ||
| description: 'Specific provider to test' | ||
| required: false | ||
| default: 'all' | ||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
| env: | ||
| COVERAGE_THRESHOLD: 60 | ||
| PYTHONUNBUFFERED: 1 | ||
| jobs: | ||
| # === Dependency Security Scanning === | ||
| dependency-scan: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Install pip-audit | ||
| run: pip install pip-audit | ||
| - name: Run dependency vulnerability scan | ||
| run: pip-audit --desc --format json > dependency-report.json || true | ||
| - name: Upload dependency report | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: dependency-report | ||
| path: dependency-report.json | ||
| # === Code Quality (Linting) === | ||
| lint: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Install linting tools | ||
| run: | | ||
| pip install mypy ruff black isort | ||
| - name: Black formatting check | ||
| run: black --check --fast --target-version py311 terradev_cli/ tests/ || true | ||
| - name: isort import ordering check | ||
| run: isort --check-only terradev_cli/ tests/ || true | ||
| - name: MyPy strict type checking | ||
| run: mypy terradev_cli/ --strict --ignore-missing-imports || true | ||
| - name: Ruff linting (BLE, F401, E501 - auto-fixable) | ||
| run: ruff check terradev_cli/ tests/ --fix || true | ||
| continue-on-error: true | ||
| # === Rust Extension Build === | ||
| build-rust: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Install Rust toolchain | ||
| uses: dtolnay/rust-toolchain@stable | ||
| - name: Cache Rust build artifacts | ||
| uses: actions/cache@v4 | ||
| with: | ||
| path: | | ||
| ~/.cargo/registry | ||
| ~/.cargo/git | ||
| rust/target | ||
| key: ${{ runner.os }}-cargo-${{ hashFiles('rust/Cargo.lock') }} | ||
| restore-keys: ${{ runner.os }}-cargo- | ||
| - name: Install maturin | ||
| run: pip install "maturin>=1.4,<2.0" | ||
| - name: Build Rust extensions | ||
| run: bash scripts/build_rust.sh --dev | ||
| - name: Verify imports | ||
| run: | | ||
| python -c " | ||
| import terradev_dag_executor | ||
| import terradev_credential_vault | ||
| import terradev_gpu_topology | ||
| import terradev_semantic_router | ||
| print('All Rust extensions imported successfully') | ||
| " | ||
| # === Unit Tests (Linux) === | ||
| test-linux: | ||
| runs-on: ubuntu-latest | ||
| needs: [build-rust] | ||
| strategy: | ||
| matrix: | ||
| python-version: ['3.10', '3.11', '3.12', '3.13'] | ||
| env: | ||
| TERRADEV_SKIP_ONBOARDING: "1" | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| submodules: false | ||
| - name: Set up Python ${{ matrix.python-version }} | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: ${{ matrix.python-version }} | ||
| cache: 'pip' | ||
| - name: Clear pip cache | ||
| run: | | ||
| pip cache purge | ||
| - name: Install Rust toolchain | ||
| uses: dtolnay/rust-toolchain@stable | ||
| - name: Cache Rust build artifacts | ||
| uses: actions/cache@v4 | ||
| with: | ||
| path: | | ||
| ~/.cargo/registry | ||
| ~/.cargo/git | ||
| rust/target | ||
| key: ${{ runner.os }}-cargo-${{ matrix.python-version }}-${{ hashFiles('rust/Cargo.lock') }} | ||
| restore-keys: ${{ runner.os }}-cargo- | ||
| - name: Install package + test deps | ||
| run: | | ||
| pip install -e ".[dev]" | ||
| pip install pytest pytest-asyncio pytest-cov pytest-xdist | ||
| pip install "maturin>=1.4,<2.0" | ||
| - name: Build and install Rust extensions | ||
| run: bash scripts/build_rust.sh --dev | ||
| - name: CLI smoke test | ||
| run: | | ||
| terradev --version | ||
| terradev --help | ||
| - name: Check for legacy duplicate files | ||
| run: | | ||
| if find terradev_cli -type f \( -name "*_old.py" -o -name "*_backup.py" -o -name "*_fixed.py" -o -name "*_simple.py" -o -name "*_final.py" -o -name "*_clean.py" -o -name "*_tiered.py" \) | grep -q .; then | ||
| echo "ERROR: Legacy duplicate files found" | ||
| exit 1 | ||
| fi | ||
| - name: Run unit tests with coverage | ||
| run: | | ||
| timeout 600 pytest tests/ -v --tb=short \ | ||
| --cov=terradev_cli --cov-report=xml --cov-report=html \ | ||
| --cov-fail-under=${{ env.COVERAGE_THRESHOLD }} \ | ||
| --ignore=tests/test_mcp_handlers.py | ||
| - name: Provider conformance tests | ||
| run: | | ||
| timeout 300 pytest tests/test_provider_conformance.py -v --tb=short | ||
| - name: CLI-specific coverage report | ||
| run: | | ||
| echo "=== CLI Module Coverage ===" | ||
| timeout 300 pytest tests/ -v --tb=short --cov=terradev_cli.cli --cov-report=term-missing --ignore=tests/test_mcp_handlers.py || true | ||
| echo "==========================" | ||
| - name: Upload coverage to Codecov | ||
| uses: codecov/codecov-action@v4 | ||
| with: | ||
| file: ./coverage.xml | ||
| flags: unittests-linux-${{ matrix.python-version }} | ||
| name: codecov-linux-${{ matrix.python-version }} | ||
| fail_ci_if_error: false | ||
| continue-on-error: true | ||
| # === CLI Functionality Tests === | ||
| cli-functionality: | ||
| runs-on: ubuntu-latest | ||
| needs: [lint] | ||
| env: | ||
| TERRADEV_SKIP_ONBOARDING: "1" | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Install package | ||
| run: pip install -e ".[dev]" | ||
| - name: Test CLI help commands | ||
| run: | | ||
| terradev --help | ||
| terradev quote --help | ||
| terradev provision --help | ||
| terradev status --help | ||
| terradev configure --help | ||
| - name: Test quote command with invalid GPU | ||
| run: | | ||
| OUTPUT=$(terradev quote -g invalid_gpu_name 2>&1 || true) | ||
| echo "$OUTPUT" | ||
| if echo "$OUTPUT" | grep -q "ERROR\|error\|No quotes"; then | ||
| echo "✓ Error handling works for invalid GPU" | ||
| else | ||
| echo "✗ Error handling failed for invalid GPU" | ||
| exit 1 | ||
| fi | ||
| - name: Test quote command without credentials | ||
| run: | | ||
| OUTPUT=$(terradev quote -g a100 -p runpod 2>&1 || true) | ||
| echo "$OUTPUT" | ||
| if echo "$OUTPUT" | grep -q "credentials\|configure\|No quotes"; then | ||
| echo "✓ Credential error prompt works" | ||
| else | ||
| echo "✗ Credential error prompt failed" | ||
| exit 1 | ||
| fi | ||
| - name: Test configure command validation | ||
| run: | | ||
| # Test configure without provider | ||
| OUTPUT=$(terradev configure 2>&1 || true) | ||
| if echo "$OUTPUT" | grep -q "provider\|required"; then | ||
| echo "✓ Configure validation works" | ||
| else | ||
| echo "✗ Configure validation failed" | ||
| exit 1 | ||
| fi | ||
| - name: Test status command without instances | ||
| run: | | ||
| OUTPUT=$(terradev status 2>&1 || true) | ||
| echo "$OUTPUT" | ||
| # Should handle gracefully even with no instances | ||
| echo "✓ Status command handles empty state" | ||
| # === Error Prompt Validation === | ||
| error-prompt-tests: | ||
| runs-on: ubuntu-latest | ||
| needs: [lint] | ||
| env: | ||
| TERRADEV_SKIP_ONBOARDING: "1" | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Install package | ||
| run: pip install -e ".[dev]" | ||
| - name: Test missing credentials error message | ||
| run: | | ||
| rm -rf ~/.terradev/credentials.json 2>/dev/null || true | ||
| OUTPUT=$(terradev quote -g a100 -p runpod 2>&1 || true) | ||
| echo "$OUTPUT" | ||
| # Check for helpful error message | ||
| if echo "$OUTPUT" | grep -qi "configure\|credentials\|setup"; then | ||
| echo "✓ Helpful credentials error message" | ||
| else | ||
| echo "✗ Unhelpful error message" | ||
| exit 1 | ||
| fi | ||
| - name: Test invalid provider error message | ||
| run: | | ||
| OUTPUT=$(terradev quote -g a100 -p nonexistent_provider 2>&1 || true) | ||
| echo "$OUTPUT" | ||
| if echo "$OUTPUT" | grep -qi "provider\|available\|supported"; then | ||
| echo "✓ Helpful provider error message" | ||
| else | ||
| echo "✗ Unhelpful provider error message" | ||
| exit 1 | ||
| fi | ||
| - name: Test malformed GPU type error | ||
| run: | | ||
| OUTPUT=$(terradev quote -g "" 2>&1 || true) | ||
| echo "$OUTPUT" | ||
| if echo "$OUTPUT" | grep -qi "required\|gpu\|type"; then | ||
| echo "✓ Helpful GPU type error message" | ||
| else | ||
| echo "✗ Unhelpful GPU type error message" | ||
| exit 1 | ||
| fi | ||
| - name: Test provision without credentials error | ||
| run: | | ||
| OUTPUT=$(terradev provision -g a100 2>&1 || true) | ||
| echo "$OUTPUT" | ||
| if echo "$OUTPUT" | grep -qi "credentials\|configure\|quote"; then | ||
| echo "✓ Helpful provision error message" | ||
| else | ||
| echo "✗ Unhelpful provision error message" | ||
| exit 1 | ||
| fi | ||
| # === Provider Mock Tests === | ||
| provider-mock-tests: | ||
| runs-on: ubuntu-latest | ||
| needs: [lint] | ||
| env: | ||
| TERRADEV_SKIP_ONBOARDING: "1" | ||
| MOCK_MODE: "true" | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Install package | ||
| run: pip install -e ".[dev]" | ||
| - name: Test provider imports | ||
| run: | | ||
| python -c " | ||
| from terradev_cli.providers import LambdaLabsProvider, RunPodProvider | ||
| from terradev_cli.providers.base_provider import BaseProvider | ||
| from terradev_cli.providers.types import QuoteRequest, ProvisionRequest | ||
| print('✓ All provider imports successful') | ||
| " | ||
| - name: Test provider initialization | ||
| run: | | ||
| python -c " | ||
| from terradev_cli.providers import LambdaLabsProvider, RunPodProvider | ||
| lambda_provider = LambdaLabsProvider({}) | ||
| runpod_provider = RunPodProvider({}) | ||
| print('✓ Provider initialization successful') | ||
| " | ||
| - name: Test GPU catalog normalization | ||
| run: | | ||
| python -c " | ||
| from terradev_cli.providers.gpu_catalog import normalize | ||
| result = normalize('A100') | ||
| assert result is not None, 'GPU normalization failed' | ||
| print('✓ GPU catalog normalization works') | ||
| " | ||
| - name: Test typed API contracts | ||
| run: | | ||
| python -c " | ||
| from terradev_cli.providers.types import QuoteRequest, ProvisionRequest, GPUDescriptor, GPUVendor | ||
| gpu = GPUDescriptor(name='A100', vendor=GPUVendor.NVIDIA, vram_gb=80) | ||
| quote_req = QuoteRequest(gpu=gpu, region='us-east-1') | ||
| print('✓ Typed API contracts work') | ||
| " | ||
| # === Package Installation Tests === | ||
| package-installation: | ||
| runs-on: ubuntu-latest | ||
| needs: [build] | ||
| strategy: | ||
| matrix: | ||
| python-version: ['3.10', '3.11', '3.12', '3.13'] | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: ${{ matrix.python-version }} | ||
| - name: Download built package | ||
| uses: actions/download-artifact@v4 | ||
| with: | ||
| name: dist | ||
| path: dist/ | ||
| - name: Install from wheel | ||
| run: | | ||
| pip install dist/*.whl | ||
| terradev --version | ||
| - name: Test CLI after installation | ||
| run: | | ||
| terradev --help | ||
| terradev quote --help | ||
| - name: Test module imports | ||
| run: | | ||
| python -c "import terradev_cli; print('✓ terradev_cli imports')" | ||
| python -c "from terradev_cli.providers import BaseProvider; print('✓ providers imports')" | ||
| python -c "from terradev_cli.providers.types import Quote; print('✓ types imports')" | ||
| - name: Uninstall and reinstall from sdist | ||
| run: | | ||
| pip uninstall -y terradev-cli | ||
| pip install dist/*.tar.gz | ||
| terradev --version | ||
| # === Unit Tests (Windows) === | ||
| test-windows: | ||
| runs-on: windows-latest | ||
| strategy: | ||
| matrix: | ||
| python-version: ['3.10', '3.11', '3.12'] | ||
| env: | ||
| TERRADEV_SKIP_ONBOARDING: "1" | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Set up Python ${{ matrix.python-version }} | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: ${{ matrix.python-version }} | ||
| cache: 'pip' | ||
| - name: Install package + test deps | ||
| run: | | ||
| pip install -e ".[dev]" | ||
| pip install pytest pytest-asyncio pytest-cov | ||
| - name: CLI smoke test | ||
| run: | | ||
| terradev --version | ||
| terradev --help | ||
| - name: Run unit tests (Windows) | ||
| run: pytest tests/ -v --tb=short --ignore=tests/test_mcp_handlers.py --ignore=tests/test_integration.py | ||
| # === Unit Tests (macOS) === | ||
| test-macos: | ||
| runs-on: macos-latest | ||
| strategy: | ||
| matrix: | ||
| python-version: ['3.10', '3.11', '3.12'] | ||
| env: | ||
| TERRADEV_SKIP_ONBOARDING: "1" | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Set up Python ${{ matrix.python-version }} | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: ${{ matrix.python-version }} | ||
| cache: 'pip' | ||
| - name: Install package + test deps | ||
| run: | | ||
| pip install -e ".[dev]" | ||
| pip install pytest pytest-asyncio pytest-cov | ||
| - name: CLI smoke test | ||
| run: | | ||
| terradev --version | ||
| terradev --help | ||
| - name: Run unit tests (macOS) | ||
| run: pytest tests/ -v --tb=short --ignore=tests/test_mcp_handlers.py --ignore=tests/test_integration.py | ||
| # === Build & Verify Package === | ||
| build: | ||
| runs-on: ubuntu-latest | ||
| needs: [lint, test-linux] | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Build wheel + sdist | ||
| run: | | ||
| pip install build | ||
| python -m build | ||
| - name: Verify package contents | ||
| run: | | ||
| python -c " | ||
| import zipfile | ||
| import glob | ||
| whl_file = glob.glob('dist/*.whl')[0] | ||
| z = zipfile.ZipFile(whl_file) | ||
| files = z.namelist() | ||
| required = [ | ||
| 'terradev_cli/providers/', | ||
| 'terradev_cli/terraform/', | ||
| 'terradev_cli/kubernetes/', | ||
| 'terradev_cli/README.md', | ||
| 'terradev_cli/core/semantic_signals/routing_policy.yaml', | ||
| 'terradev_cli/integrations/', | ||
| ] | ||
| missing = [r for r in required if not any(r in f for f in files)] | ||
| if missing: | ||
| print(f'Missing required files: {missing}') | ||
| print('Files in wheel:') | ||
| for f in sorted(files): | ||
| print(f' {f}') | ||
| exit(1) | ||
| print('✓ Package contents verified') | ||
| print(f'Total files in wheel: {len(files)}') | ||
| " | ||
| - name: Verify built package installs | ||
| run: | | ||
| pip install dist/*.whl | ||
| terradev --version | ||
| - name: Upload artifacts | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: dist | ||
| path: dist/ | ||
| retention-days: 7 | ||
| # === Security Scanning === | ||
| security: | ||
| runs-on: ubuntu-latest | ||
| needs: [build] | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Install package for scan | ||
| run: pip install -e ".[dev]" | ||
| - name: Install + scan (non-blocking) | ||
| run: | | ||
| pip install bandit safety | ||
| bandit -r terradev_cli/ -f json -o bandit-report.json -ll || true | ||
| safety scan --json > safety-report.json || true | ||
| continue-on-error: true | ||
| - name: Upload security reports | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: security-reports | ||
| path: | | ||
| bandit-report.json | ||
| safety-report.json | ||
| # === Integration Tests (Mock) === | ||
| integration-mock: | ||
| runs-on: ubuntu-latest | ||
| needs: [lint] | ||
| env: | ||
| MOCK: "true" | ||
| TERRADEV_SKIP_ONBOARDING: "1" | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Install package + test deps | ||
| run: | | ||
| pip install -e ".[dev]" | ||
| pip install pytest pytest-asyncio | ||
| - name: Run integration tests (mock) | ||
| run: python tests/test_integration.py --mock --suite providers | ||
| # === Live Integration Tests (Manual) === | ||
| integration-live: | ||
| runs-on: ubuntu-latest | ||
| needs: [lint, security] | ||
| if: github.event_name == 'workflow_dispatch' && inputs.run_integration == true | ||
| env: | ||
| TERRADEV_SKIP_ONBOARDING: "1" | ||
| AWS_ACCESS_KEY_ID: ${{ secrets.AWS_TEST_KEY }} | ||
| AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_TEST_SECRET }} | ||
| RUNPOD_API_KEY: ${{ secrets.RUNPOD_TEST_KEY }} | ||
| VAST_API_KEY: ${{ secrets.VAST_TEST_KEY }} | ||
| LAMBDA_API_KEY: ${{ secrets.LAMBDA_TEST_KEY }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Install package + test deps | ||
| run: | | ||
| pip install -e ".[dev]" | ||
| pip install pytest pytest-asyncio | ||
| - name: Run live integration tests | ||
| run: | | ||
| if [ "${{ inputs.test_provider }}" = "all" ]; then | ||
| python tests/test_integration.py --suite providers | ||
| else | ||
| python tests/test_integration.py --suite providers --provider ${{ inputs.test_provider }} | ||
| fi | ||
| continue-on-error: true | ||
| # === Performance Benchmarking === | ||
| benchmark: | ||
| runs-on: ubuntu-latest | ||
| needs: [test-linux] | ||
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Install package + test deps | ||
| run: | | ||
| pip install -e ".[dev]" | ||
| pip install pytest pytest-asyncio | ||
| - name: Run performance benchmarks | ||
| run: python tests/test_integration.py --performance --suite performance | ||
| # === Publish to TestPyPI (Staging) === | ||
| publish-test: | ||
| if: github.event_name == 'push' && github.ref == 'refs/heads/develop' | ||
| needs: [test-linux, test-windows, test-macos, build, security, cli-functionality, error-prompt-tests, provider-mock-tests, package-installation] | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| id-token: write | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Build | ||
| run: | | ||
| pip install build | ||
| python -m build | ||
| - name: Publish to TestPyPI | ||
| uses: pypa/gh-action-pypi-publish@release/v1 | ||
| with: | ||
| password: ${{ secrets.TEST_PYPI_API_TOKEN }} | ||
| repository-url: https://test.pypi.org/legacy/ | ||
| - name: Install from TestPyPI and verify | ||
| run: | | ||
| pip install --index-url https://test.pypi.org/simple/ terradev-cli | ||
| terradev --version | ||
| terradev --help | ||
| # === Publish to PyPI (Production) === | ||
| publish: | ||
| if: startsWith(github.ref, 'refs/tags/v') | ||
| needs: [test-linux, test-windows, test-macos, build, security, cli-functionality, error-prompt-tests, provider-mock-tests, package-installation] | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| id-token: write | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Build | ||
| run: | | ||
| pip install build | ||
| python -m build | ||
| - name: Publish to PyPI | ||
| uses: pypa/gh-action-pypi-publish@release/v1 | ||
| with: | ||
| password: ${{ secrets.PYPI_API_TOKEN }} | ||
| - name: Install from PyPI and verify | ||
| run: | | ||
| pip install terradev-cli | ||
| terradev --version | ||
| terradev --help | ||