diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..10582bc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,42 @@ +version: 2 + +# Modeled on the ZeroPath monorepo (cunningham) conventions: monthly cadence, +# grouped updates, cooldowns to keep churn low, and a dedicated group for +# security updates so those can be reviewed/merged quickly. +updates: + - package-ecosystem: "pip" # covers this uv/pyproject project (reads uv.lock) + directory: "/" + schedule: + interval: "monthly" + time: "07:00" + timezone: "America/Los_Angeles" + cooldown: + default-days: 14 + semver-major-days: 30 + semver-minor-days: 21 + semver-patch-days: 14 + open-pull-requests-limit: 2 + groups: + python-dependencies: + patterns: + - "*" + python-security: + applies-to: "security-updates" + patterns: + - "*" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + time: "08:00" + timezone: "America/Los_Angeles" + open-pull-requests-limit: 1 + groups: + github-actions: + patterns: + - "*" + github-actions-security: + applies-to: "security-updates" + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5b71ac7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,132 @@ +name: CI + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + +# Cancel superseded runs on the same ref (e.g. rapid pushes to a PR). +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + # Pinned tool versions so local runs match CI. Keep in sync with pyproject. + RUFF_VERSION: "0.15.1" + PYRIGHT_VERSION: "1.1.411" + +jobs: + lint: + name: ruff (lint + format) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Install uv + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: true + + - name: Ruff lint + run: uvx ruff@${{ env.RUFF_VERSION }} check --output-format=github . + + - name: Ruff format check + run: uvx ruff@${{ env.RUFF_VERSION }} format --check --diff . + + typecheck: + name: pyright + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Install uv + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: true + + # Resolve project deps (--frozen: fail if uv.lock is stale) so pyright + # can see mcp/requests types, then add pyright to the ephemeral env. + - name: Run pyright + run: uv run --frozen --python 3.12 --with pyright==${{ env.PYRIGHT_VERSION }} pyright + + test: + name: pytest (py${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13"] + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Install uv + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: true + + - name: Run tests + run: uv run --frozen --python ${{ matrix.python-version }} pytest -v --tb=short + + build: + name: build + verify package + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Install uv + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: true + + - name: Build sdist + wheel + run: uv build + + # On a version tag, the built version must match the tag (v0.2.0 -> 0.2.0). + - name: Verify tag matches package version + if: startsWith(github.ref, 'refs/tags/v') + run: | + tag="${GITHUB_REF_NAME#v}" + pkg="$(uv run --frozen python -c 'import zeropath_mcp_server as z; print(z.__version__)')" + echo "tag=$tag package=$pkg" + if [ "$tag" != "$pkg" ]; then + echo "::error::Release tag ($tag) does not match package version ($pkg)" + exit 1 + fi + + - name: Validate distribution metadata + run: uvx twine check dist/* + + - name: Smoke-test the built wheel + run: | + uv run --isolated --no-project --with dist/*.whl \ + python -c "import zeropath_mcp_server as z; print('import OK, version', z.__version__)" + + - name: Upload build artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: dist + path: dist/* + if-no-files-found: error + + # Single required status: green only when every matrix leg + job above passed. + ci-ok: + name: CI passed + if: always() + needs: [lint, typecheck, test, build] + runs-on: ubuntu-latest + steps: + - name: Verify all jobs succeeded + run: | + if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then + echo "One or more CI jobs failed." + exit 1 + fi + echo "All CI jobs passed." diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000..567c77a --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,39 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + +# Requires the ANTHROPIC_API_KEY repository secret. Until it is set the review +# step is skipped (the job still succeeds), so the workflow is inert rather +# than red on repos/forks without the secret configured. +jobs: + claude-review: + # Skip PRs from forks: secrets are not available to fork PRs anyway. + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + id-token: write + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + if: ${{ env.ANTHROPIC_API_KEY != '' }} + uses: anthropics/claude-code-action@ff9acae5886d41a99ed4ec14b7dc147d55834722 # v1 + with: + anthropic_api_key: ${{ env.ANTHROPIC_API_KEY }} + allowed_bots: "cursor" + plugin_marketplaces: "https://github.com/anthropics/claude-code.git" + plugins: "code-review@claude-code-plugins" + prompt: "/code-review:code-review --comment ${{ github.repository }}/pull/${{ github.event.pull_request.number }}" + claude_args: '--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"' diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..9174a1e --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,45 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +# Responds to @claude mentions in issues / PR comments / reviews. +# Requires the ANTHROPIC_API_KEY repository secret; inert (skipped) without it. +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write + id-token: write + actions: read # Required for Claude to read CI results on PRs + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + if: ${{ env.ANTHROPIC_API_KEY != '' }} + uses: anthropics/claude-code-action@ff9acae5886d41a99ed4ec14b7dc147d55834722 # v1 + with: + anthropic_api_key: ${{ env.ANTHROPIC_API_KEY }} + # Allows Claude to read CI results on PRs. + additional_permissions: | + actions: read diff --git a/pyproject.toml b/pyproject.toml index a4c409f..edd14e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,16 @@ testpaths = ["tests"] python_files = ["test_*.py"] python_functions = ["test_*"] +# Linting/formatting is enforced in CI with ruff (pinned in .github/workflows/ci.yml). +[tool.ruff] +line-length = 120 +target-version = "py312" + +[tool.pyright] +# Type-checked in CI with pyright against the uv-resolved environment. +include = ["src", "tests"] +pythonVersion = "3.12" + [dependency-groups] dev = [ "pytest==9.0.2",