diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c446f14..763531e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.12" cache: pip @@ -36,15 +36,12 @@ jobs: - name: Lint (ruff) run: ruff check . - - name: Collect tests (no Ollama required) - # test_api.py is an integration smoke test that requires a live Ollama server. - # We collect-only to confirm the file is importable and has no syntax errors. - # Set SKIP_INTEGRATION=1 as a convention for future test guards. + - name: Run unit tests (no Ollama required) env: SKIP_INTEGRATION: "1" run: | pip install pytest - python -m pytest test_api.py --collect-only -q || true + python -m pytest test_server.py -q chrome-bridge: name: chrome-bridge (Node.js) @@ -57,7 +54,7 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: "22" cache: npm diff --git a/.github/workflows/provenance.yml b/.github/workflows/provenance.yml new file mode 100644 index 0000000..b73a408 --- /dev/null +++ b/.github/workflows/provenance.yml @@ -0,0 +1,141 @@ +name: Provenance + +on: + workflow_dispatch: + push: + branches: + - main + - master + paths: + - ".github/workflows/provenance.yml" + - ".github/workflows/ci.yml" + - "README.md" + - "SETUP.md" + - "mkdocs.yml" + - "docs/**" + - "api-server/**" + - "chrome-bridge/**" + - "chrome-demo/**" + - "python-mediapipe/**" + pull_request: + paths: + - ".github/workflows/provenance.yml" + - ".github/workflows/ci.yml" + - "README.md" + - "SETUP.md" + - "mkdocs.yml" + - "docs/**" + - "api-server/**" + - "chrome-bridge/**" + - "chrome-demo/**" + - "python-mediapipe/**" + +permissions: + contents: read + attestations: write + id-token: write + artifact-metadata: write + +jobs: + attest-demo-bundle: + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + attestations: write + id-token: write + artifact-metadata: write + env: + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: api-server/requirements.txt + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: "22" + cache: npm + cache-dependency-path: chrome-bridge/package-lock.json + + - name: Validate api-server + working-directory: api-server + env: + SKIP_INTEGRATION: "1" + run: | + pip install -r requirements.txt + pip install pytest ruff + ruff check . + python -m pytest test_server.py -q + + - name: Validate chrome-bridge + working-directory: chrome-bridge + run: | + npm ci --ignore-scripts + npm test + + - name: Build docs site + run: | + pip install mkdocs-material + mkdocs build --strict + + - name: Create Gemini Nano demo bundle + run: | + tar -czf "gemini-nano-demo-bundle-${GITHUB_SHA}.tar.gz" \ + README.md \ + SETUP.md \ + mkdocs.yml \ + docs \ + site \ + api-server/server.py \ + api-server/requirements.txt \ + api-server/start.ps1 \ + chrome-bridge/server.js \ + chrome-bridge/package.json \ + chrome-bridge/package-lock.json \ + chrome-bridge/start.ps1 \ + chrome-demo/index.html \ + python-mediapipe/requirements.txt \ + python-mediapipe/run_nano.py + sha256sum "gemini-nano-demo-bundle-${GITHUB_SHA}.tar.gz" | tee "gemini-nano-demo-bundle-${GITHUB_SHA}.sha256" + + - name: Attest Gemini Nano demo bundle + id: attest + uses: actions/attest@f6bf1532d7d6793fce74eac584813a8eee607999 # v4 + with: + subject-path: "gemini-nano-demo-bundle-${{ github.sha }}.tar.gz" + + - name: Verify current run attestation + run: | + gh attestation verify "gemini-nano-demo-bundle-${GITHUB_SHA}.tar.gz" \ + --repo Coding-Autopilot-System/gemini-nano \ + --signer-workflow Coding-Autopilot-System/gemini-nano/.github/workflows/provenance.yml \ + --source-ref "${GITHUB_REF}" + + - name: Publish Gemini Nano demo bundle + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: gemini-nano-demo-bundle + path: | + gemini-nano-demo-bundle-${{ github.sha }}.tar.gz + gemini-nano-demo-bundle-${{ github.sha }}.sha256 + + - name: Publish Gemini Nano attestation bundle + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: gemini-nano-attestation-bundle + path: ${{ steps.attest.outputs.bundle-path }} + + - name: Write provenance summary + run: | + { + echo "## Gemini Nano Provenance" + echo + echo "- Artifact: \`gemini-nano-demo-bundle-${GITHUB_SHA}.tar.gz\`" + echo "- Attestation: ${{ steps.attest.outputs.attestation-url }}" + echo "- Bundle artifact: \`gemini-nano-attestation-bundle\`" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/README.md b/README.md index e644d71..33e2346 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ graph TD cd api-server python -m venv .venv && .venv\Scripts\activate # Windows pip install -r requirements.txt -python app.py +python server.py # → http://localhost:8080 ``` diff --git a/api-server/test_api.py b/api-server/test_api.py index 26f70bc..16fcf99 100644 --- a/api-server/test_api.py +++ b/api-server/test_api.py @@ -1,4 +1,4 @@ -"""Quick test — calls both models and prints responses.""" +"""Manual integration smoke script for a live local LLM API server.""" import httpx BASE = "http://localhost:9000" diff --git a/api-server/test_server.py b/api-server/test_server.py new file mode 100644 index 0000000..58da31d --- /dev/null +++ b/api-server/test_server.py @@ -0,0 +1,32 @@ +from fastapi import HTTPException +from fastapi.testclient import TestClient + +from server import app, resolve + + +def test_resolve_accepts_documented_aliases() -> None: + assert resolve("nano") == "gemma3:1b" + assert resolve("best") == "gemma3:12b" + assert resolve("qwen") == "qwen3:8b" + + +def test_resolve_rejects_unknown_models() -> None: + try: + resolve("missing-model") + except HTTPException as exc: + assert exc.status_code == 404 + assert "Unknown model" in str(exc.detail) + else: + raise AssertionError("resolve() should reject unknown models") + + +def test_list_models_endpoint_exposes_catalog() -> None: + client = TestClient(app) + + response = client.get("/v1/models") + + assert response.status_code == 200 + payload = response.json() + ids = {model["id"] for model in payload["data"]} + assert payload["object"] == "list" + assert {"gemma3:12b", "qwen3:8b", "gemma3:1b", "phi3:mini"} <= ids