Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
name: CI

# Execution-based test gate for the sample apps.
#
# Every app is built (compile + typecheck) and then driven with a smoke check
# that boots the built app and asserts it responds. Pinecone is MOCKED or GATED
# with a dummy key — CI needs no live Pinecone connection and no real API key
# (per AGENTS.md). See TESTING.md for the pattern and the manual
# "run against real Pinecone" path.
#
# The LIVE validation lane (real Pinecone in CI) is intentionally NOT here yet:
# it is blocked on PIN-50 (PINECONE_API_KEY secret, owner jhamon@pinecone.io).
# Add it as a separate, secret-gated job when PIN-50 lands.

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

env:
# Dummy, non-secret placeholders. The Pinecone clients only require the key to
# be a non-empty string at construction; no live call is made in these checks.
PINECONE_API_KEY: pclocal-ci-dummy-key
NODE_VERSION: "20"

jobs:
legal-semantic-search:
name: legal-semantic-search (Next.js)
runs-on: ubuntu-latest
defaults:
run:
working-directory: legal-semantic-search
env:
PINECONE_INDEX: ci-dummy-index
VOYAGE_API_KEY: ci-dummy-voyage
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
# Committed lockfile predates a langchain peer-dep bump; --legacy-peer-deps
# installs it the way the app is documented to install.
- run: npm install --legacy-peer-deps
- name: Build (next build — compiles + typechecks)
run: npm run build
- name: Smoke — boot app, expect HTTP 200 on /
run: bash ../scripts/ci/smoke_http.sh "http://127.0.0.1:3000/" 200 -- npx next start -p 3000

namespace-notes-client:
name: namespace-notes / client (Next.js)
runs-on: ubuntu-latest
defaults:
run:
working-directory: namespace-notes/client
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- run: npm install --legacy-peer-deps
- name: Build (next build — compiles + typechecks)
run: npm run build
- name: Smoke — boot app, expect HTTP 200 on /
run: bash ../../scripts/ci/smoke_http.sh "http://127.0.0.1:3000/" 200 -- npx next start -p 3000

namespace-notes-server:
name: namespace-notes / server (Express + TS)
runs-on: ubuntu-latest
defaults:
run:
working-directory: namespace-notes/server
env:
PORT: "4001"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
# `npm run build` runs `npm install --include=dev && tsc` per the repo.
- name: Build (install + tsc)
run: npm run build
# The server has no Pinecone-free route, so the smoke asserts the process
# boots, binds the port, and routes a request (any HTTP response = alive;
# the handler itself 500s because Pinecone is gated with a dummy key).
- name: Smoke — boot server, expect any HTTP response
run: bash ../../scripts/ci/smoke_http.sh "http://127.0.0.1:4001/api/documents/files/ci-smoke" any -- node dist/index.js

pinecone-assistant:
name: pinecone-assistant (Next.js + pnpm)
runs-on: ubuntu-latest
defaults:
run:
working-directory: pinecone-assistant
env:
# Skip pnpm's pre-run dependency verification, which otherwise fails the
# run on optional native build scripts (sharp) being blocked in CI.
NPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: "false"
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
cache-dependency-path: pinecone-assistant/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
- name: Build (next build — compiles + typechecks)
run: pnpm run build
- name: Smoke — boot app, expect HTTP 200 on /
run: bash ../scripts/ci/smoke_http.sh "http://127.0.0.1:3000/" 200 -- pnpm exec next start -p 3000

shop-the-look-web:
name: shop-the-look / web (Next.js)
runs-on: ubuntu-latest
defaults:
run:
working-directory: shop-the-look
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- run: npm ci
- name: Build (next build — compiles + typechecks)
run: npm run build
- name: Smoke — boot app, expect HTTP 200 on /
run: bash ../scripts/ci/smoke_http.sh "http://127.0.0.1:3000/" 200 -- npx next start -p 3000

shop-the-look-api:
name: shop-the-look / api (FastAPI + Python)
runs-on: ubuntu-latest
defaults:
run:
working-directory: shop-the-look
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install (runtime + test deps)
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt -r requirements-dev.txt
# Execution-based smoke: boots the FastAPI app via TestClient with Pinecone
# mocked, and asserts the root route serves. No live Pinecone needed.
- name: Smoke — pytest (Pinecone mocked)
run: python -m pytest tests/ -q
102 changes: 102 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Testing & CI — the sample-apps test gate

This repo ships **no versioned artifact**. Its value is that every sample app
still **compiles, boots, and behaves**. CI enforces that with an
**execution-based test gate**: each app is built and then *driven* while
running. This document is the reusable pattern — other Pinecone example repos
should adopt the same shape.

## What CI does

Workflow: [`.github/workflows/ci.yml`](.github/workflows/ci.yml). One job per
app (they run in parallel):

| App | Build | Smoke / behavior check |
|-----|-------|------------------------|
| `legal-semantic-search` | `next build` | boot `next start`, expect **HTTP 200** on `/` |
| `namespace-notes/client` | `next build` | boot `next start`, expect **HTTP 200** on `/` |
| `namespace-notes/server` | `npm install --include=dev && tsc` | boot `node dist/index.js`, expect **any HTTP response** (process alive + routing) |
| `pinecone-assistant` | `next build` (pnpm) | boot `next start`, expect **HTTP 200** on `/` |
| `shop-the-look/web` | `next build` | boot `next start`, expect **HTTP 200** on `/` |
| `shop-the-look/api` | pip install | `pytest` boots the FastAPI app (Pinecone mocked) and asserts the root route serves |

**Build** catches the failure that matters most for an example: source that no
longer compiles or typechecks against its pinned Pinecone client. **Smoke** then
proves the built app actually starts and serves — not just that `tsc`/`next
build` was happy.

## Pinecone is mocked or gated — never live in CI

Per the repo `AGENTS.md` rule, CI does **not** connect to Pinecone:

- **Node apps** get a **dummy** `PINECONE_API_KEY` (`pclocal-ci-dummy-key`). The
Pinecone TS client only requires the key to be a non-empty string at
construction, so the app boots; the homepage smoke never triggers a live call.
The `namespace-notes` server *does* reach Pinecone on its document routes, so
its smoke accepts **any** HTTP response — a `500` from the gated client still
proves the process booted and routes requests.
- **Python (`shop-the-look/api`)** mocks `pinecone.Pinecone` in
[`shop-the-look/tests/test_smoke.py`](shop-the-look/tests/test_smoke.py) and
sets dummy `PINECONE_*` env, then drives the app with FastAPI's `TestClient`.

No real key is ever committed or required for the gate.

## The reusable smoke helper

[`scripts/ci/smoke_http.sh`](scripts/ci/smoke_http.sh) boots a server command,
polls an HTTP endpoint until it responds (or times out), then shuts it down:

```bash
scripts/ci/smoke_http.sh <url> <200|any> -- <command to start the server...>
```

- `200` — require HTTP 200 (static homepage that doesn't need Pinecone).
- `any` — accept any HTTP response (proves the process booted and is routing,
even if the handler errors because Pinecone is gated).

Reuse this in any Node/Python example repo that boots an HTTP server.

## Run the gate locally

```bash
# Node app (example: legal-semantic-search)
cd legal-semantic-search
npm install --legacy-peer-deps
PINECONE_API_KEY=pclocal-ci-dummy-key PINECONE_INDEX=ci-dummy-index VOYAGE_API_KEY=ci-dummy \
npm run build
PINECONE_API_KEY=pclocal-ci-dummy-key \
bash ../scripts/ci/smoke_http.sh "http://127.0.0.1:3000/" 200 -- npx next start -p 3000

# Python api (shop-the-look)
cd shop-the-look
pip install -r requirements.txt -r requirements-dev.txt
python -m pytest tests/ -q
```

## Run against real Pinecone (manual)

The gate above is intentionally offline. To exercise an app end-to-end against a
real Pinecone project, follow the app's own README, then export real credentials
in your shell instead of the dummy values:

```bash
export PINECONE_API_KEY="<your real key>" # from https://app.pinecone.io
export PINECONE_INDEX="<your index>" # legal-semantic-search
export PINECONE_INDEX_NAME="<your index>" # shop-the-look/api
export PINECONE_TOP_K=5 # shop-the-look/api
# ...plus any app-specific keys (VOYAGE_API_KEY, OpenAI, Google, etc.)
npm run dev # or: uvicorn api.index:app --reload (shop-the-look/api)
```

For the Python smoke, running against real Pinecone means dropping the
`mock.patch("pinecone.Pinecone")` in `tests/test_smoke.py` and exporting the real
`PINECONE_*` env before `pytest`.

## Live CI lane — blocked on PIN-50

A **live** validation lane (real Pinecone in CI, gated on a `PINECONE_API_KEY`
repository secret) is deliberately **not** wired up yet. It is blocked on
**PIN-50** (provision `PINECONE_API_KEY`, owner: jhamon@pinecone.io). When PIN-50
lands, add a separate job that runs only when the secret is present (self-skips
otherwise), following the `langchain-retrieval-agent-example` / `examples`
pattern from the DevRel OSS inventory.
4 changes: 2 additions & 2 deletions namespace-notes/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
"description": "Sample app for namespace notes",
"scripts": {
"install:client": "cd client && npm install",
"install:server": "cd server/node && npm install",
"install:server": "cd server && npm install",
"install:all": "npm run install:client && npm run install:server",
"start:client": "cd client && npm start",
"start:server": "cd server/node && npm start"
"start:server": "cd server && npm start"
},
"devDependencies": {
"concurrently": "^8.2.2"
Expand Down
53 changes: 53 additions & 0 deletions scripts/ci/smoke_http.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
#
# Boot a server, wait until an HTTP endpoint responds, then shut it down.
# This is the reusable "drive the running app" step of the sample-apps test gate.
# Pinecone is expected to be MOCKED or GATED (dummy key) by the caller — this
# script never needs a live Pinecone connection.
#
# Usage:
# smoke_http.sh <url> <expect> -- <command to start server...>
# <url> URL to poll (e.g. http://127.0.0.1:3000/)
# <expect> "200" to require HTTP 200, or "any" to accept any HTTP response
# ("any" proves the process booted and is routing, even if the
# handler errors because Pinecone is gated).
#
set -uo pipefail

URL="${1:?url required}"
EXPECT="${2:?expect required}"
shift 2
[ "${1:-}" = "--" ] && shift

echo "::group::smoke: booting server for $URL (expect=$EXPECT)"
"$@" &
PID=$!
cleanup() { kill "$PID" 2>/dev/null; wait "$PID" 2>/dev/null; }
trap cleanup EXIT

ATTEMPTS=60
for i in $(seq 1 "$ATTEMPTS"); do
if ! kill -0 "$PID" 2>/dev/null; then
echo "::error::server process exited before responding"
echo "::endgroup::"
exit 1
fi
# curl prints '000' via -w on connection failure; do not append another.
CODE=$(curl -s -o /dev/null -w '%{http_code}' "$URL" 2>/dev/null)
CODE=${CODE:-000}
if [ "$CODE" != "000" ]; then
if [ "$EXPECT" = "any" ] || [ "$CODE" = "$EXPECT" ]; then
echo "smoke OK: $URL -> HTTP $CODE (attempt $i)"
echo "::endgroup::"
exit 0
fi
echo " attempt $i: got HTTP $CODE, want $EXPECT — retrying"
else
echo " attempt $i: no response yet — retrying"
fi
sleep 2
done

echo "::error::smoke FAILED: $URL never returned expected status ($EXPECT) after $ATTEMPTS attempts"
echo "::endgroup::"
exit 1
3 changes: 3 additions & 0 deletions shop-the-look/requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Test-only dependencies for the execution-based smoke gate (see TESTING.md).
pytest==8.3.4
httpx==0.28.1
35 changes: 35 additions & 0 deletions shop-the-look/tests/test_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""
Execution-based smoke test for the Shop The Look FastAPI backend.

Proves the app boots and serves requests with Pinecone **mocked** — no live
Pinecone connection and no API key required (per the repo AGENTS.md rule that
Pinecone calls are mocked or gated when no key is present in CI).

`api/deps.py` constructs `Pinecone(...)` and `pc.Index(...)` at import time, and
`api/config.py` reads several env vars at import time, so we set dummy env and
patch `pinecone.Pinecone` before importing the app.

To run against REAL Pinecone instead, see TESTING.md ("run against real
Pinecone"): export real PINECONE_* env vars and delete the patch.
"""
import os
from unittest import mock

# Dummy env consumed at import time by api/config.py. Never a real key.
os.environ.setdefault("PINECONE_API_KEY", "pclocal-ci-dummy-key")
os.environ.setdefault("PINECONE_INDEX_NAME", "ci-dummy-index")
os.environ.setdefault("PINECONE_TOP_K", "5")


def test_app_boots_and_serves_root_with_mocked_pinecone():
with mock.patch("pinecone.Pinecone") as MockPinecone:
MockPinecone.return_value.Index.return_value = mock.MagicMock()

from api.index import app
from fastapi.testclient import TestClient

client = TestClient(app)
resp = client.get("/api")

assert resp.status_code == 200, resp.text
assert "Shop The Look" in resp.json()["message"]
Loading