From ae3ab18eb9d59ff228eeb59921efddb3c503991f Mon Sep 17 00:00:00 2001 From: Himanshu Kumar Date: Sat, 18 Jul 2026 00:54:58 +0530 Subject: [PATCH 1/5] ci: add CI, coverage, and CodeQL workflows --- .github/workflows/ci.yml | 58 ++++++++++++++++++++++++++++++++ .github/workflows/codeql.yml | 38 +++++++++++++++++++++ .github/workflows/coverage.yml | 60 ++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e0e9cd8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: CI + +on: + push: + branches: ["**"] + tags-ignore: ["v*.*.*"] + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + quality-and-test: + name: Quality & tests (${{ matrix.os }}, py${{ matrix.python-version }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.9", "3.10", "3.11", "3.12"] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Sync dependencies + run: uv sync --extra dev --extra ollama + + # Step 3: Lint + format check + - name: Ruff check + run: uv run ruff check src tests + + - name: Ruff format check + run: uv run ruff format --check src tests + + # Step 4: Type check (strict) + - name: Mypy + run: uv run mypy src + + # Step 5: Security scan + - name: Bandit + run: uv run bandit -r src -c pyproject.toml + + # Step 6: Tests (integration auto-skips without Ollama) + - name: Pytest + run: uv run pytest diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..bc0faff --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,38 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "27 4 * * 1" + +permissions: + contents: read + +jobs: + analyze: + name: Analyze (CodeQL) + runs-on: ubuntu-latest + permissions: + security-events: write + packages: read + strategy: + fail-fast: false + matrix: + language: ["python"] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: security-and-quality + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..2a1c0bd --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,60 @@ +name: Coverage + +on: + push: + branches: [main] + tags-ignore: ["v*.*.*"] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + coverage: + name: Coverage (Ubuntu, py3.12) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Set up Python + run: uv python install 3.12 + + - name: Sync dependencies + run: uv sync --extra dev --extra ollama + + - name: Run tests with coverage + run: | + uv run pytest --cov=modeldock --cov-report=term-missing \ + --cov-report=xml:coverage.xml --cov-report=html:htmlcov + + - name: Upload coverage XML + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-xml + path: coverage.xml + if-no-files-found: ignore + + - name: Upload HTML coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: htmlcov + path: htmlcov + if-no-files-found: ignore + + - name: Upload to Codecov + if: always() + uses: codecov/codecov-action@v4 + with: + files: coverage.xml + fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From 4647d87c14d7b637688d9653ee5ee8ed6ef9239d Mon Sep 17 00:00:00 2001 From: Himanshu Kumar Date: Sat, 18 Jul 2026 00:56:27 +0530 Subject: [PATCH 2/5] ci: add congratulatory comment workflow for merged PRs --- .github/workflows/congrats.yml | 95 ++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 .github/workflows/congrats.yml diff --git a/.github/workflows/congrats.yml b/.github/workflows/congrats.yml new file mode 100644 index 0000000..61aa10b --- /dev/null +++ b/.github/workflows/congrats.yml @@ -0,0 +1,95 @@ +name: Congratulate on Merge + +# Automatically posts a congratulatory comment on merged Pull Requests. +# +# Features: +# - First-contribution detection via GitHub API +# - Label-based enhancements (first contribution, good first issue) +# - Bot author filtering (skips automated accounts) +# - Structured logging for audit trail +# +# Trigger: pull_request_target → closed (only merged PRs targeting main) +# Permissions: pull-requests: write, contents: read (least privilege) + +on: + pull_request_target: + types: [closed] + branches: [main] + +permissions: + pull-requests: write + contents: read + +jobs: + congratulate: + name: Congratulate contributor + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - name: Log event + run: | + echo "PR #${{ github.event.pull_request.number }} merged by ${{ github.event.pull_request.user.login }}" + echo "Title: ${{ github.event.pull_request.title }}" + + - name: Skip bots + id: check + env: + AUTHOR: ${{ github.event.pull_request.user.login }} + AUTHOR_TYPE: ${{ github.event.pull_request.user.type }} + run: | + if [ "$AUTHOR_TYPE" = "Bot" ]; then + echo "Author $AUTHOR is a bot — skipping congratulations." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Detect first contribution + if: steps.check.outputs.skip == 'false' + id: first + env: + GH_TOKEN: ${{ github.token }} + AUTHOR: ${{ github.event.pull_request.user.login }} + REPO: ${{ github.repository }} + run: | + COUNT=$(gh api "repos/$REPO/pulls?state=all&creator=$AUTHOR" \ + --jq 'length') + echo "Total PRs by $AUTHOR (incl. this one): $COUNT" + if [ "$COUNT" -le 1 ]; then + echo "first=true" >> "$GITHUB_OUTPUT" + else + echo "first=false" >> "$GITHUB_OUTPUT" + fi + + - name: Add labels + if: steps.check.outputs.skip == 'false' && steps.first.outputs.first == 'true' + env: + GH_TOKEN: ${{ github.token }} + PR: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + gh pr edit "$PR" --add-label "first contribution" + echo "Labeled PR #$PR as 'first contribution'." + + - name: Post congratulatory comment + if: steps.check.outputs.skip == 'false' + env: + GH_TOKEN: ${{ github.token }} + PR: ${{ github.event.pull_request.number }} + AUTHOR: ${{ github.event.pull_request.user.login }} + FIRST: ${{ steps.first.outputs.first }} + REPO: ${{ github.repository }} + run: | + if [ "$FIRST" = "true" ]; then + BODY=":tada: @$AUTHOR — congratulations on your **first contribution** to ModelDock! :tada + + Your PR has been merged. We're thrilled to have you in the community — thank you for helping make local AI model management better for everyone. + + If you're looking for what to tackle next, check out issues labeled \`good first issue\`." + else + BODY=":tada: Thanks @$AUTHOR — your PR has been merged! :tada + + Appreciate the contribution to ModelDock. If you spot anything else to improve, we'd love another PR." + fi + gh pr comment "$PR" --body "$BODY" + echo "Posted congratulatory comment to PR #$PR." From b69995ac050eca619419476f3fc91aeee4182370 Mon Sep 17 00:00:00 2001 From: Himanshu Kumar Date: Sat, 18 Jul 2026 00:58:35 +0530 Subject: [PATCH 3/5] style: run ruff format on 3 files to fix CI --- tests/conftest.py | 4 +--- tests/unit/test_core.py | 4 +--- tests/unit/test_sdk_api.py | 8 ++------ 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index d91430f..6895579 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -120,9 +120,7 @@ def list_installed(self) -> List[ModelRef]: return list(self._installed) def is_installed(self, ref: ModelRef) -> bool: - return any( - r.name == ref.name and r.tag == ref.tag for r in self._installed - ) + return any(r.name == ref.name and r.tag == ref.tag for r in self._installed) def pull(self, ref: ModelRef, progress: Any = None) -> PullResult: if self.pull_should_fail: diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 4541904..34e1ccf 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -20,9 +20,7 @@ def test_download_pull_records_on_success( assert fake_cache.is_fresh(ref) -def test_download_pull_raises_on_failure( - fake_runtime: object, fake_cache: object -) -> None: +def test_download_pull_raises_on_failure(fake_runtime: object, fake_cache: object) -> None: fake_runtime.pull_should_fail = True svc = DownloadService(fake_runtime, fake_cache) with pytest.raises(DownloadError): diff --git a/tests/unit/test_sdk_api.py b/tests/unit/test_sdk_api.py index 5f58265..9e8b241 100644 --- a/tests/unit/test_sdk_api.py +++ b/tests/unit/test_sdk_api.py @@ -87,9 +87,7 @@ def test_sdk_remove_via_fake_manager() -> None: from tests.conftest import FakeCache, FakeRegistry, FakeRuntime runtime = FakeRuntime() - mgr = md.ModelManager( - runtime=runtime, registry=FakeRegistry(), cache=FakeCache() - ) + mgr = md.ModelManager(runtime=runtime, registry=FakeRegistry(), cache=FakeCache()) mgr.install("llama3") mgr.remove("llama3") assert not mgr.verify("llama3") @@ -99,9 +97,7 @@ def test_sdk_load_via_fake_manager() -> None: from tests.conftest import FakeCache, FakeRegistry, FakeRuntime runtime = FakeRuntime() - mgr = md.ModelManager( - runtime=runtime, registry=FakeRegistry(), cache=FakeCache() - ) + mgr = md.ModelManager(runtime=runtime, registry=FakeRegistry(), cache=FakeCache()) client = mgr.load("llama3", auto_install=True) assert client == {"client": "llama3:latest"} From b323b0bba02105eac24553b306aa0ea7ff036dcd Mon Sep 17 00:00:00 2001 From: Himanshu Kumar Date: Sat, 18 Jul 2026 01:03:20 +0530 Subject: [PATCH 4/5] fix: remove redundant cast in config._toml_load to satisfy mypy on py3.9/3.10 --- src/modeldock/common/config.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modeldock/common/config.py b/src/modeldock/common/config.py index 79b94ca..d62030e 100644 --- a/src/modeldock/common/config.py +++ b/src/modeldock/common/config.py @@ -9,7 +9,7 @@ import os from pathlib import Path -from typing import Any, Dict, Optional, cast +from typing import Any, Dict, Optional from pydantic import BaseModel, Field, field_validator @@ -81,7 +81,8 @@ def _toml_load(path: Path) -> Dict[str, Any]: import tomli with path.open("rb") as fh: - return cast(Dict[str, Any], tomli.load(fh)) + data: Dict[str, Any] = tomli.load(fh) + return data def _coerce_log_level(value: Any) -> str: From 71d8fd49bc7acb9bf772cb5c74f026150d354fa7 Mon Sep 17 00:00:00 2001 From: Himanshu Kumar Date: Sat, 18 Jul 2026 01:08:32 +0530 Subject: [PATCH 5/5] ci: consolidate ci/coverage/codeql into single check.yml; keep congrats.yml separate --- .github/workflows/check.yml | 129 +++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 58 --------------- .github/workflows/codeql.yml | 38 ---------- .github/workflows/coverage.yml | 60 --------------- 4 files changed, 129 insertions(+), 156 deletions(-) create mode 100644 .github/workflows/check.yml delete mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/codeql.yml delete mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..cc763f3 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,129 @@ +name: Check + +on: + push: + branches: ["**"] + tags-ignore: ["v*.*.*"] + pull_request: + branches: [main] + schedule: + - cron: "27 4 * * 1" + +permissions: + contents: read + +concurrency: + group: check-${{ github.ref }} + cancel-in-progress: true + +jobs: + quality-and-test: + name: Quality & tests (${{ matrix.os }}, py${{ matrix.python-version }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.9", "3.10", "3.11", "3.12"] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Sync dependencies + run: uv sync --extra dev --extra ollama + + - name: Ruff check + run: uv run ruff check src tests + + - name: Ruff format check + run: uv run ruff format --check src tests + + - name: Mypy + run: uv run mypy src + + - name: Bandit + run: uv run bandit -r src -c pyproject.toml + + - name: Pytest + run: uv run pytest + + coverage: + name: Coverage (Ubuntu, py3.12) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Set up Python + run: uv python install 3.12 + + - name: Sync dependencies + run: uv sync --extra dev --extra ollama + + - name: Run tests with coverage + run: | + uv run pytest --cov=modeldock --cov-report=term-missing \ + --cov-report=xml:coverage.xml --cov-report=html:htmlcov + + - name: Upload coverage XML + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-xml + path: coverage.xml + if-no-files-found: ignore + + - name: Upload HTML coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: htmlcov + path: htmlcov + if-no-files-found: ignore + + - name: Upload to Codecov + if: always() + uses: codecov/codecov-action@v4 + with: + files: coverage.xml + fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + codeql: + name: Analyze (CodeQL) + runs-on: ubuntu-latest + permissions: + security-events: write + packages: read + strategy: + fail-fast: false + matrix: + language: ["python"] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: security-and-quality + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index e0e9cd8..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: CI - -on: - push: - branches: ["**"] - tags-ignore: ["v*.*.*"] - pull_request: - branches: [main] - -permissions: - contents: read - -concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: true - -jobs: - quality-and-test: - name: Quality & tests (${{ matrix.os }}, py${{ matrix.python-version }}) - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.9", "3.10", "3.11", "3.12"] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - - - name: Set up Python ${{ matrix.python-version }} - run: uv python install ${{ matrix.python-version }} - - - name: Sync dependencies - run: uv sync --extra dev --extra ollama - - # Step 3: Lint + format check - - name: Ruff check - run: uv run ruff check src tests - - - name: Ruff format check - run: uv run ruff format --check src tests - - # Step 4: Type check (strict) - - name: Mypy - run: uv run mypy src - - # Step 5: Security scan - - name: Bandit - run: uv run bandit -r src -c pyproject.toml - - # Step 6: Tests (integration auto-skips without Ollama) - - name: Pytest - run: uv run pytest diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index bc0faff..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: CodeQL - -on: - push: - branches: [main] - pull_request: - branches: [main] - schedule: - - cron: "27 4 * * 1" - -permissions: - contents: read - -jobs: - analyze: - name: Analyze (CodeQL) - runs-on: ubuntu-latest - permissions: - security-events: write - packages: read - strategy: - fail-fast: false - matrix: - language: ["python"] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - queries: security-and-quality - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml deleted file mode 100644 index 2a1c0bd..0000000 --- a/.github/workflows/coverage.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Coverage - -on: - push: - branches: [main] - tags-ignore: ["v*.*.*"] - pull_request: - branches: [main] - -permissions: - contents: read - -jobs: - coverage: - name: Coverage (Ubuntu, py3.12) - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - - - name: Set up Python - run: uv python install 3.12 - - - name: Sync dependencies - run: uv sync --extra dev --extra ollama - - - name: Run tests with coverage - run: | - uv run pytest --cov=modeldock --cov-report=term-missing \ - --cov-report=xml:coverage.xml --cov-report=html:htmlcov - - - name: Upload coverage XML - if: always() - uses: actions/upload-artifact@v4 - with: - name: coverage-xml - path: coverage.xml - if-no-files-found: ignore - - - name: Upload HTML coverage report - if: always() - uses: actions/upload-artifact@v4 - with: - name: htmlcov - path: htmlcov - if-no-files-found: ignore - - - name: Upload to Codecov - if: always() - uses: codecov/codecov-action@v4 - with: - files: coverage.xml - fail_ci_if_error: false - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}