Skip to content

Commit feb71cf

Browse files
OgeonX-Airoot
andauthored
Add attested provenance workflow for the Gemini Nano demo bundle (#9)
* ci(provenance): attest gemini nano demo bundle * fix(provenance): harden gemini nano bundle validation * ci(provenance): validate attestation workflow on pull requests * test(api-server): add real provenance-safe unit checks Replace brittle pytest collection on the manual smoke script with a real unit suite for alias resolution and model catalog exposure. Align CI and provenance verification so both workflows use the same non-Ollama Python checks. * ci(workflows): refresh deprecated node setup action Update the chrome-bridge CI job to the current setup-node pin already used by the provenance workflow so GitHub stops forcing the deprecated Node 20 action runtime. * ci(workflows): refresh deprecated python setup action Align the CI Python setup step with the current action pin already used in the provenance workflow to remove the remaining deprecated Node 20 runtime warning. --------- Co-authored-by: root <root@Kimi.localdomain>
1 parent bc3479b commit feb71cf

5 files changed

Lines changed: 179 additions & 9 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
steps:
2222
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
2323

24-
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
24+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
2525
with:
2626
python-version: "3.12"
2727
cache: pip
@@ -36,15 +36,12 @@ jobs:
3636
- name: Lint (ruff)
3737
run: ruff check .
3838

39-
- name: Collect tests (no Ollama required)
40-
# test_api.py is an integration smoke test that requires a live Ollama server.
41-
# We collect-only to confirm the file is importable and has no syntax errors.
42-
# Set SKIP_INTEGRATION=1 as a convention for future test guards.
39+
- name: Run unit tests (no Ollama required)
4340
env:
4441
SKIP_INTEGRATION: "1"
4542
run: |
4643
pip install pytest
47-
python -m pytest test_api.py --collect-only -q || true
44+
python -m pytest test_server.py -q
4845
4946
chrome-bridge:
5047
name: chrome-bridge (Node.js)
@@ -57,7 +54,7 @@ jobs:
5754
steps:
5855
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
5956

60-
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
57+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
6158
with:
6259
node-version: "22"
6360
cache: npm

.github/workflows/provenance.yml

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
name: Provenance
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches:
7+
- main
8+
- master
9+
paths:
10+
- ".github/workflows/provenance.yml"
11+
- ".github/workflows/ci.yml"
12+
- "README.md"
13+
- "SETUP.md"
14+
- "mkdocs.yml"
15+
- "docs/**"
16+
- "api-server/**"
17+
- "chrome-bridge/**"
18+
- "chrome-demo/**"
19+
- "python-mediapipe/**"
20+
pull_request:
21+
paths:
22+
- ".github/workflows/provenance.yml"
23+
- ".github/workflows/ci.yml"
24+
- "README.md"
25+
- "SETUP.md"
26+
- "mkdocs.yml"
27+
- "docs/**"
28+
- "api-server/**"
29+
- "chrome-bridge/**"
30+
- "chrome-demo/**"
31+
- "python-mediapipe/**"
32+
33+
permissions:
34+
contents: read
35+
attestations: write
36+
id-token: write
37+
artifact-metadata: write
38+
39+
jobs:
40+
attest-demo-bundle:
41+
runs-on: ubuntu-latest
42+
timeout-minutes: 20
43+
permissions:
44+
contents: read
45+
attestations: write
46+
id-token: write
47+
artifact-metadata: write
48+
env:
49+
GH_TOKEN: ${{ github.token }}
50+
steps:
51+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
52+
53+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
54+
with:
55+
python-version: "3.12"
56+
cache: pip
57+
cache-dependency-path: api-server/requirements.txt
58+
59+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
60+
with:
61+
node-version: "22"
62+
cache: npm
63+
cache-dependency-path: chrome-bridge/package-lock.json
64+
65+
- name: Validate api-server
66+
working-directory: api-server
67+
env:
68+
SKIP_INTEGRATION: "1"
69+
run: |
70+
pip install -r requirements.txt
71+
pip install pytest ruff
72+
ruff check .
73+
python -m pytest test_server.py -q
74+
75+
- name: Validate chrome-bridge
76+
working-directory: chrome-bridge
77+
run: |
78+
npm ci --ignore-scripts
79+
npm test
80+
81+
- name: Build docs site
82+
run: |
83+
pip install mkdocs-material
84+
mkdocs build --strict
85+
86+
- name: Create Gemini Nano demo bundle
87+
run: |
88+
tar -czf "gemini-nano-demo-bundle-${GITHUB_SHA}.tar.gz" \
89+
README.md \
90+
SETUP.md \
91+
mkdocs.yml \
92+
docs \
93+
site \
94+
api-server/server.py \
95+
api-server/requirements.txt \
96+
api-server/start.ps1 \
97+
chrome-bridge/server.js \
98+
chrome-bridge/package.json \
99+
chrome-bridge/package-lock.json \
100+
chrome-bridge/start.ps1 \
101+
chrome-demo/index.html \
102+
python-mediapipe/requirements.txt \
103+
python-mediapipe/run_nano.py
104+
sha256sum "gemini-nano-demo-bundle-${GITHUB_SHA}.tar.gz" | tee "gemini-nano-demo-bundle-${GITHUB_SHA}.sha256"
105+
106+
- name: Attest Gemini Nano demo bundle
107+
id: attest
108+
uses: actions/attest@f6bf1532d7d6793fce74eac584813a8eee607999 # v4
109+
with:
110+
subject-path: "gemini-nano-demo-bundle-${{ github.sha }}.tar.gz"
111+
112+
- name: Verify current run attestation
113+
run: |
114+
gh attestation verify "gemini-nano-demo-bundle-${GITHUB_SHA}.tar.gz" \
115+
--repo Coding-Autopilot-System/gemini-nano \
116+
--signer-workflow Coding-Autopilot-System/gemini-nano/.github/workflows/provenance.yml \
117+
--source-ref "${GITHUB_REF}"
118+
119+
- name: Publish Gemini Nano demo bundle
120+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
121+
with:
122+
name: gemini-nano-demo-bundle
123+
path: |
124+
gemini-nano-demo-bundle-${{ github.sha }}.tar.gz
125+
gemini-nano-demo-bundle-${{ github.sha }}.sha256
126+
127+
- name: Publish Gemini Nano attestation bundle
128+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
129+
with:
130+
name: gemini-nano-attestation-bundle
131+
path: ${{ steps.attest.outputs.bundle-path }}
132+
133+
- name: Write provenance summary
134+
run: |
135+
{
136+
echo "## Gemini Nano Provenance"
137+
echo
138+
echo "- Artifact: \`gemini-nano-demo-bundle-${GITHUB_SHA}.tar.gz\`"
139+
echo "- Attestation: ${{ steps.attest.outputs.attestation-url }}"
140+
echo "- Bundle artifact: \`gemini-nano-attestation-bundle\`"
141+
} >> "$GITHUB_STEP_SUMMARY"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ graph TD
3535
cd api-server
3636
python -m venv .venv && .venv\Scripts\activate # Windows
3737
pip install -r requirements.txt
38-
python app.py
38+
python server.py
3939
# → http://localhost:8080
4040
```
4141

api-server/test_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Quick test — calls both models and prints responses."""
1+
"""Manual integration smoke script for a live local LLM API server."""
22
import httpx
33

44
BASE = "http://localhost:9000"

api-server/test_server.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from fastapi import HTTPException
2+
from fastapi.testclient import TestClient
3+
4+
from server import app, resolve
5+
6+
7+
def test_resolve_accepts_documented_aliases() -> None:
8+
assert resolve("nano") == "gemma3:1b"
9+
assert resolve("best") == "gemma3:12b"
10+
assert resolve("qwen") == "qwen3:8b"
11+
12+
13+
def test_resolve_rejects_unknown_models() -> None:
14+
try:
15+
resolve("missing-model")
16+
except HTTPException as exc:
17+
assert exc.status_code == 404
18+
assert "Unknown model" in str(exc.detail)
19+
else:
20+
raise AssertionError("resolve() should reject unknown models")
21+
22+
23+
def test_list_models_endpoint_exposes_catalog() -> None:
24+
client = TestClient(app)
25+
26+
response = client.get("/v1/models")
27+
28+
assert response.status_code == 200
29+
payload = response.json()
30+
ids = {model["id"] for model in payload["data"]}
31+
assert payload["object"] == "list"
32+
assert {"gemma3:12b", "qwen3:8b", "gemma3:1b", "phi3:mini"} <= ids

0 commit comments

Comments
 (0)