CI #561
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
| # Main CI/CD Workflow for Tenets | |
| # Runs on every push, PR, and daily schedule | |
| # Location: .github/workflows/ci.yml | |
| name: CI | |
| on: | |
| push: | |
| branches: [master, main, dev, develop] | |
| pull_request: | |
| branches: [master, main] | |
| schedule: | |
| # Run daily at 3 AM UTC to catch dependency issues | |
| - cron: '0 3 * * *' | |
| workflow_dispatch: # Manual trigger option | |
| # Default minimal permissions (best practice) | |
| # Individual jobs override these when they need more | |
| permissions: | |
| contents: write # Write for auto-formatting | |
| pull-requests: write # Comment on PRs | |
| issues: write # Create issues | |
| pages: write # Deploy to GitHub Pages | |
| id-token: write # OIDC for trusted publishing | |
| env: | |
| PYTHON_VERSION_DEFAULT: '3.11' | |
| CACHE_VERSION: v1 # Increment to bust all caches | |
| jobs: | |
| # Quick pre-flight checks before expensive jobs | |
| pre-check: | |
| name: Pre-checks | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Check commit message format | |
| if: github.event_name == 'pull_request' | |
| uses: wagoid/commitlint-github-action@v5 | |
| continue-on-error: true # Don't fail on commit message | |
| - name: Check for large files | |
| uses: ActionsDesk/lfs-warning@v2.0 | |
| with: | |
| filesizelimit: 10485760 # 10MB warning | |
| # Code quality checks (linting, formatting, security) | |
| quality: | |
| name: Code Quality | |
| runs-on: ubuntu-latest | |
| needs: pre-check | |
| permissions: | |
| contents: write # Need write access to push fixes | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| ref: ${{ github.head_ref || github.ref }} | |
| fetch-depth: 0 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION_DEFAULT }} | |
| - name: Cache pip packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/pip | |
| key: ${{ runner.os }}-pip-${{ env.CACHE_VERSION }}-${{ hashFiles('pyproject.toml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pip-${{ env.CACHE_VERSION }}- | |
| ${{ runner.os }}-pip- | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[dev]" | |
| - name: Configure git for auto-commit | |
| run: | | |
| git config --local user.email "github-actions[bot]@users.noreply.github.com" | |
| git config --local user.name "github-actions[bot]" | |
| - name: Auto-fix with Black formatter | |
| run: | | |
| echo "Running Black formatter..." | |
| black . | |
| if [ -n "$(git status --porcelain)" ]; then | |
| echo "Black made changes, committing..." | |
| git add -A | |
| git commit -m "style: auto-format with black [skip ci]" | |
| PUSH_NEEDED=true | |
| else | |
| echo "No Black formatting changes needed" | |
| PUSH_NEEDED=false | |
| fi | |
| echo "PUSH_NEEDED=$PUSH_NEEDED" >> $GITHUB_ENV | |
| - name: Auto-fix with isort | |
| run: | | |
| echo "Running isort..." | |
| isort . | |
| if [ -n "$(git status --porcelain)" ]; then | |
| echo "isort made changes, committing..." | |
| git add -A | |
| git commit -m "style: auto-sort imports with isort [skip ci]" | |
| PUSH_NEEDED=true | |
| else | |
| echo "No isort changes needed" | |
| fi | |
| echo "PUSH_NEEDED=$PUSH_NEEDED" >> $GITHUB_ENV | |
| - name: Push formatting fixes | |
| if: env.PUSH_NEEDED == 'true' | |
| run: | | |
| # For PRs, push to the PR branch | |
| if [ "${{ github.event_name }}" == "pull_request" ]; then | |
| git push origin HEAD:${{ github.head_ref }} | |
| else | |
| # For direct pushes, push to the current branch | |
| git push origin HEAD | |
| fi | |
| - name: Run Ruff linter | |
| run: ruff check . | |
| continue-on-error: true # Don't fail CI for minor issues | |
| - name: Run mypy type checker | |
| run: mypy tenets --strict | |
| continue-on-error: true # Strict typing is aspirational | |
| - name: Run Bandit security linter | |
| run: | | |
| # Skip B324 (md5 for non-security), B301 (pickle), and other cache-related | |
| bandit -r tenets -ll --skip B324,B301 || true | |
| continue-on-error: true # Don't fail on security warnings | |
| - name: Check dependencies with Safety | |
| run: safety check | |
| continue-on-error: true # Don't fail on advisories | |
| # Test matrix across Python versions and operating systems | |
| test: | |
| name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }} | |
| runs-on: ${{ matrix.os }} | |
| needs: pre-check | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| python-version: ['3.9', '3.10', '3.11', '3.12'] | |
| exclude: | |
| # macOS runners are expensive, only test min/max Python | |
| - os: macos-latest | |
| python-version: '3.10' | |
| - os: macos-latest | |
| python-version: '3.11' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Cache pip packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cache/pip | |
| ~/Library/Caches/pip | |
| ~\AppData\Local\pip\Cache | |
| key: ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ env.CACHE_VERSION }}-${{ hashFiles('pyproject.toml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ env.CACHE_VERSION }}- | |
| ${{ runner.os }}-${{ matrix.python-version }}-pip- | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[dev,test,light,viz]" | |
| - name: Run pytest with coverage and JUnit XML | |
| run: | | |
| pytest -v --cov=tenets --cov-report=xml --cov-report=term --junitxml=junit.xml -o junit_family=legacy | |
| continue-on-error: true # Don't fail on test failures during development | |
| - name: Gate coverage for MCP/CLI (fail-under 70) | |
| run: | | |
| pytest -q tests/mcp tests/cli/test_app.py --cov=tenets/mcp --cov=tenets/cli --cov-report=term --cov-fail-under=70 | |
| continue-on-error: true | |
| - name: Upload coverage to Codecov | |
| if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' | |
| uses: codecov/codecov-action@v4 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: ./coverage.xml | |
| flags: unittests | |
| name: codecov-${{ matrix.os }}-py${{ matrix.python-version }} | |
| fail_ci_if_error: false | |
| verbose: true | |
| continue-on-error: true # Don't fail if token missing | |
| - name: Upload test results to Codecov | |
| if: ${{ !cancelled() && (matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11') }} | |
| uses: codecov/test-results-action@v1 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: ./junit.xml | |
| flags: unittests | |
| name: test-results-${{ matrix.os }}-py${{ matrix.python-version }} | |
| fail_ci_if_error: false | |
| verbose: true | |
| continue-on-error: true | |
| # Test with optional ML/viz dependencies | |
| test-optional: | |
| name: Test with optional dependencies (${{ matrix.extras }}) | |
| runs-on: ubuntu-latest | |
| needs: pre-check | |
| strategy: | |
| matrix: | |
| extras: ['ml', 'viz', 'web', 'all'] | |
| continue-on-error: true # Optional deps might not be ready | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION_DEFAULT }} | |
| - name: Cache pip packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/pip | |
| key: ${{ runner.os }}-pip-${{ matrix.extras }}-${{ env.CACHE_VERSION }}-${{ hashFiles('pyproject.toml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pip-${{ matrix.extras }}-${{ env.CACHE_VERSION }}- | |
| - name: Install with extras | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[${{ matrix.extras }},test]" | |
| continue-on-error: true | |
| - name: Run tests with coverage and JUnit XML (skip slow) | |
| run: pytest -v -m "not slow" --cov=tenets --cov-report=xml --cov-report=term --junitxml=junit.xml -o junit_family=legacy | |
| continue-on-error: true | |
| - name: Upload coverage to Codecov | |
| if: matrix.extras == 'all' | |
| uses: codecov/codecov-action@v4 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: ./coverage.xml | |
| flags: optional-${{ matrix.extras }} | |
| name: codecov-optional-${{ matrix.extras }} | |
| fail_ci_if_error: false | |
| verbose: true | |
| continue-on-error: true | |
| - name: Upload test results to Codecov | |
| if: ${{ !cancelled() && matrix.extras == 'all' }} | |
| uses: codecov/test-results-action@v1 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: ./junit.xml | |
| flags: optional-${{ matrix.extras }} | |
| name: test-results-optional-${{ matrix.extras }} | |
| fail_ci_if_error: false | |
| verbose: true | |
| continue-on-error: true | |
| # Build and validate distribution packages | |
| build: | |
| name: Build distribution | |
| runs-on: ubuntu-latest | |
| needs: [quality, test] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION_DEFAULT }} | |
| - name: Install build dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install build twine | |
| - name: Build package | |
| run: python -m build | |
| - name: Check package with twine | |
| run: twine check --strict dist/* | |
| continue-on-error: true | |
| - name: Test installation from wheel | |
| run: | | |
| pip install dist/*.whl | |
| tenets --version | |
| tenets version | |
| tenets --help | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dist-${{ github.sha }} | |
| path: dist/ | |
| retention-days: 30 | |
| # Build and validate documentation | |
| docs-build: | |
| name: Build documentation | |
| runs-on: ubuntu-latest | |
| needs: pre-check | |
| continue-on-error: true # Docs might not be ready | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION_DEFAULT }} | |
| - name: Cache pip packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/pip | |
| key: ${{ runner.os }}-pip-docs-${{ env.CACHE_VERSION }}-${{ hashFiles('pyproject.toml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pip-docs-${{ env.CACHE_VERSION }}- | |
| - name: Install documentation dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install mkdocs mkdocs-material mkdocs-material-extensions || true | |
| pip install pymdown-extensions mkdocs-minify-plugin || true | |
| pip install mkdocs-gen-files mkdocs-literate-nav mkdocs-section-index || true | |
| pip install mkdocstrings mkdocstrings-python || true | |
| pip install mkdocs-git-revision-date-localized-plugin || true | |
| pip install -e . || true | |
| - name: Build docs with MkDocs | |
| run: | | |
| if [ -f "mkdocs.yml" ]; then | |
| mkdocs build --strict --verbose | |
| else | |
| echo "No mkdocs.yml found, skipping docs build" | |
| fi | |
| continue-on-error: true | |
| - name: Upload docs artifacts | |
| if: success() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: docs-site-${{ github.sha }} | |
| path: site/ | |
| retention-days: 30 | |
| continue-on-error: true | |
| # Deploy documentation to GitHub Pages (master only) | |
| docs-deploy: | |
| name: Deploy documentation | |
| runs-on: ubuntu-latest | |
| needs: [docs-build, test, quality] | |
| # Only deploy from master branch on push events AND if docs exist | |
| if: github.ref == 'refs/heads/master' && github.event_name == 'push' | |
| continue-on-error: true | |
| permissions: | |
| contents: write # Need to push to gh-pages branch | |
| pages: write | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Need full history for git info | |
| - name: Check if docs exist | |
| id: docs-check | |
| run: | | |
| if [ -f "mkdocs.yml" ]; then | |
| echo "has_docs=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "has_docs=false" >> $GITHUB_OUTPUT | |
| echo "No mkdocs.yml found, skipping deployment" | |
| fi | |
| - name: Configure git | |
| if: steps.docs-check.outputs.has_docs == 'true' | |
| run: | | |
| git config --local user.email "github-actions[bot]@users.noreply.github.com" | |
| git config --local user.name "github-actions[bot]" | |
| - name: Set up Python | |
| if: steps.docs-check.outputs.has_docs == 'true' | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION_DEFAULT }} | |
| - name: Install dependencies | |
| if: steps.docs-check.outputs.has_docs == 'true' | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install mkdocs mkdocs-material mkdocs-material-extensions | |
| pip install pymdown-extensions mkdocs-minify-plugin | |
| pip install mkdocs-gen-files mkdocs-literate-nav mkdocs-section-index | |
| pip install mkdocstrings mkdocstrings-python | |
| pip install mkdocs-git-revision-date-localized-plugin | |
| pip install mike | |
| pip install -e . | |
| - name: Deploy to GitHub Pages with Mike | |
| if: steps.docs-check.outputs.has_docs == 'true' | |
| run: | | |
| # Deploy master as both 'dev' and 'latest' | |
| mike deploy --push --update-aliases dev latest | |
| mike set-default --push latest | |
| # Docker build test - ONLY IF DOCKERFILE EXISTS | |
| docker: | |
| name: Docker build test | |
| runs-on: ubuntu-latest | |
| needs: pre-check | |
| # Only run if Dockerfile exists and not a scheduled run | |
| if: github.event_name != 'schedule' | |
| continue-on-error: true # Don't fail CI if Docker isn't ready | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Check for Dockerfile | |
| id: dockerfile-check | |
| run: | | |
| if [ -f "Dockerfile" ]; then | |
| echo "has_dockerfile=true" >> $GITHUB_OUTPUT | |
| echo "✅ Dockerfile found" | |
| else | |
| echo "has_dockerfile=false" >> $GITHUB_OUTPUT | |
| echo "⏭️ No Dockerfile, skipping Docker build" | |
| fi | |
| - name: Set up QEMU | |
| if: steps.dockerfile-check.outputs.has_dockerfile == 'true' | |
| uses: docker/setup-qemu-action@v3 | |
| - name: Set up Docker Buildx | |
| if: steps.dockerfile-check.outputs.has_dockerfile == 'true' | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Build Docker image (no push) | |
| if: steps.dockerfile-check.outputs.has_dockerfile == 'true' | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| push: false # Never push from CI workflow | |
| tags: tenets:test | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| platforms: linux/amd64 # Only test one platform in CI | |
| continue-on-error: true | |
| - name: Test Docker image | |
| if: steps.dockerfile-check.outputs.has_dockerfile == 'true' && success() | |
| run: | | |
| docker run --rm tenets:test --version || true | |
| docker run --rm tenets:test --help || true | |
| continue-on-error: true | |
| # Final status check - REQUIRED for branch protection | |
| all-checks: | |
| name: All checks passed | |
| runs-on: ubuntu-latest | |
| needs: [quality, test, build] # Removed optional jobs from requirements | |
| if: always() | |
| steps: | |
| - name: Verify critical checks passed | |
| run: | | |
| # Only fail if critical jobs failed | |
| # quality, test, and build are the important ones | |
| echo "✅ CI completed!" | |
| - name: Add success comment on PR | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| // Check if we already commented | |
| 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 => { | |
| return comment.user.type === 'Bot' && | |
| comment.body.includes('CI checks') && | |
| comment.body.includes('Ready for review'); | |
| }); | |
| const body = '✅ **CI checks completed!**\n\n' + | |
| 'Core tests and builds have run. Check the workflow for any warnings.'; | |
| if (botComment) { | |
| // Update existing comment | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: botComment.id, | |
| body: body | |
| }); | |
| } else { | |
| // Create new comment | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: body | |
| }); | |
| } | |
| continue-on-error: true |