-
Notifications
You must be signed in to change notification settings - Fork 10
291 lines (251 loc) · 11.7 KB
/
Copy pathdeploy-lambda.yml
File metadata and controls
291 lines (251 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
name: Deploy Lambda
on:
push:
branches: [master]
paths:
- 'graqle/**'
- 'pyproject.toml'
- '.github/workflows/deploy-lambda.yml'
workflow_dispatch:
env:
LAMBDA_FUNCTION: cognigraph-api
S3_BUCKET: graqle-graphs-eu
AWS_REGION: eu-central-1
jobs:
# ── Step 1: Full Graqle import scan ──
graqle-check:
name: Graqle Import & Quality Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install graqle with all Lambda extras
run: |
python -m pip install --upgrade pip
pip install -e ".[server,api,studio,dev]"
- name: Full import scan — catch all ImportErrors
run: |
python -c "
import importlib, pkgutil, sys
errors = []
for importer, modname, ispkg in pkgutil.walk_packages(
path=['graqle'],
prefix='graqle.',
onerror=lambda name: errors.append(name),
):
try:
importlib.import_module(modname)
except Exception as e:
errors.append(f'{modname}: {e}')
if errors:
print('IMPORT ERRORS FOUND:')
for e in errors:
print(f' !! {e}')
sys.exit(1)
else:
print(f'All graqle modules import clean.')
"
- name: Verify Lambda handler loads
run: |
python -c "
from graqle.server.lambda_handler import handler
print('Lambda handler: OK')
from graqle.server.app import create_app
print('FastAPI app factory: OK')
from graqle.studio.app import mount_studio
print('Studio mount: OK')
from graqle.metrics.engine import MetricsEngine
print('MetricsEngine: OK')
"
- name: Verify Studio routes register
env:
AWS_LAMBDA_FUNCTION_NAME: test # scoped to this step — triggers /tmp path in create_app
run: |
python -c "
import os, sys, logging, traceback
logging.basicConfig(level=logging.WARNING, format='%(levelname)s %(name)s: %(message)s')
from graqle.server.app import create_app
app = create_app(graph_path='nonexistent.json', config_path='nonexistent.yaml')
# Use OpenAPI schema to enumerate routes — handles FastAPI _IncludedRouter
# wrappers (introduced in FastAPI 0.100+) that don't expose .path at the
# top level of app.routes. The schema is built from the actual route tree.
try:
schema_paths = list(app.openapi().get('paths', {}).keys())
except Exception as _e:
print(f'[warn] openapi() failed: {_e}')
schema_paths = []
# Fallback: flat walk for any Route objects still at the top level.
flat_paths = [r.path for r in app.routes if hasattr(r, 'path')]
routes = list(dict.fromkeys(schema_paths + flat_paths)) # deduplicate, preserve order
studio_routes = [r for r in routes if '/studio' in r]
print(f'Total routes: {len(routes)}')
print(f'Studio routes: {len(studio_routes)}')
# Validate required studio routes are registered.
required = ['/studio/api/health', '/studio/api/intelligence', '/studio/api/governance', '/studio/api/learning']
missing = [r for r in required if not any(p.startswith(r) for p in routes)]
for r in required:
print(f' {r}: ' + ('OK' if r not in missing else 'MISSING'))
if missing:
print('FAIL: Required studio routes missing — running direct mount_studio() diagnostic')
try:
from fastapi import FastAPI
from graqle.studio.app import mount_studio
_diag_app = FastAPI()
mount_studio(_diag_app, {'metrics': None, 'studio_available': False})
diag_paths = list(_diag_app.openapi().get('paths', {}).keys())
print(f'Direct mount_studio() paths: {diag_paths[:10]}')
if any('/studio/api' in p for p in diag_paths):
print('Direct mount_studio() succeeded — route count discrepancy is in create_app() context')
else:
print('Direct mount_studio() also missing routes — check router imports')
except Exception as _exc:
print(f'Direct mount_studio() FAILED with {type(_exc).__name__}: {_exc}')
traceback.print_exc()
print('Fix: add the missing dependency above to the pip install line in this workflow')
sys.exit(1)
"
- name: Graqle governance gate (graq verify)
run: |
python -c "
from graqle.intelligence.verify import verify_changes
from pathlib import Path
result = verify_changes(Path('.'))
print(f'Governance gate: {\"PASS\" if result else \"CHECK\"}')
"
- name: Run core tests
run: pytest tests/ -x -q --timeout=30 -k "not neo4j and not slow" 2>&1 | tail -20
# ── Step 2: Build & Deploy Lambda ──
deploy:
name: Build & Deploy to Lambda
needs: graqle-check
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Build Lambda package
run: |
mkdir -p build/lambda
# Install deps for Linux Lambda runtime.
# NOTE (CR-013, 2026-05-17): neo4j + cryptography + jsonschema +
# pyshacl + rdflib added explicitly. Without neo4j, traversal/hubs
# returns 500 (Neptune speaks Bolt). Without the others, v0.57.0
# compliance modules silently fail at import time, dropping their
# route registration. The graqle[api] extra in pyproject.toml
# already declares these, but this workflow hand-rolls a flat
# pip-install list and doesn't consult the [api] extra.
# TODO: switch to `pip install -e .[api] --target build/lambda`
# in a follow-on CR once the manylinux constraints are sorted.
# A1b (2026-06-07): pyjwt[crypto] is REQUIRED at runtime —
# graqle/studio/auth.py verifies the Cognito ID token (cross-tenant
# trust root). Without it the `import jwt` guard makes _verify_bearer
# return None for EVERY request → the fix would over-deny and break
# valid logins. cryptography (already below) is the RS256 backend.
# (Synced from public PR #191 for private/public parity.)
pip install --target build/lambda \
--platform manylinux2014_x86_64 --only-binary=:all: \
--python-version 3.10 --implementation cp \
mangum fastapi uvicorn jinja2 pydantic pydantic-settings \
pyyaml typer rich networkx numpy httpx anthropic openai \
requests \
neo4j cryptography jsonschema pyshacl rdflib \
'pyjwt[crypto]'
# Copy graqle source (not pip-installed — use repo version)
cp -r graqle/ build/lambda/graqle/
# Clean up
rm -rf build/lambda/graqle/benchmarks/ build/lambda/graqle/examples/
find build/lambda -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find build/lambda -name '*.pyc' -delete 2>/dev/null || true
# Remove boto3/botocore (Lambda runtime has these)
rm -rf build/lambda/boto3* build/lambda/botocore* build/lambda/s3transfer* 2>/dev/null || true
echo "Package size: $(du -sh build/lambda/ | cut -f1)"
- name: Create deployment zip
run: |
cd build/lambda
zip -r9 ../../lambda-deploy.zip . -x "*.pyc" "*__pycache__*"
cd ../..
echo "Zip size: $(du -sh lambda-deploy.zip | cut -f1)"
- name: Deploy to Lambda
run: |
aws lambda update-function-code \
--function-name ${{ env.LAMBDA_FUNCTION }} \
--zip-file fileb://lambda-deploy.zip \
--region ${{ env.AWS_REGION }}
- name: Wait for code deploy
run: |
aws lambda wait function-updated \
--function-name ${{ env.LAMBDA_FUNCTION }} \
--region ${{ env.AWS_REGION }}
- name: Update Lambda environment
run: |
aws lambda update-function-configuration \
--function-name ${{ env.LAMBDA_FUNCTION }} \
--environment "Variables={
COGNIGRAPH_GRAPH_PATH=s3://graqle-graphs-eu/graqle.json,
COGNIGRAPH_CONFIG_PATH=s3://graqle-graphs-eu/graqle.yaml,
COGNIGRAPH_S3_BUCKET=graqle-graphs-eu,
ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }},
NEPTUNE_ENDPOINT=graqle-kg.cluster-cfb3tqihxeti.eu-central-1.neptune.amazonaws.com,
NEPTUNE_PORT=8182,
NEPTUNE_REGION=eu-central-1,
NEPTUNE_IAM_AUTH=true
}" \
--timeout 300 \
--memory-size 1024 \
--region ${{ env.AWS_REGION }}
- name: Wait for Lambda update
run: |
aws lambda wait function-updated \
--function-name ${{ env.LAMBDA_FUNCTION }} \
--region ${{ env.AWS_REGION }}
- name: Smoke test Lambda
run: |
LAMBDA_URL=$(aws lambda get-function-url-config \
--function-name ${{ env.LAMBDA_FUNCTION }} \
--region ${{ env.AWS_REGION }} \
--query 'FunctionUrl' --output text)
echo "Lambda URL: $LAMBDA_URL"
# Wait for cold start
sleep 10
# Test health endpoint
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 "${LAMBDA_URL}health")
echo "GET /health: $HTTP_CODE"
if [ "$HTTP_CODE" != "200" ]; then
echo "FAIL: /health returned $HTTP_CODE"
exit 1
fi
# Test studio API
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 "${LAMBDA_URL}studio/api/health")
echo "GET /studio/api/health: $HTTP_CODE"
# Test graph loaded
HEALTH=$(curl -s --max-time 30 "${LAMBDA_URL}health")
echo "Health response: $HEALTH"
# Test graph visualization returns nodes
GRAPH_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 60 "${LAMBDA_URL}studio/api/graph/visualization")
echo "GET /studio/api/graph/visualization: $GRAPH_CODE"
if [ "$GRAPH_CODE" != "200" ]; then
echo "FAIL: graph visualization returned $GRAPH_CODE"
exit 1
fi
# Test Neptune health (non-blocking — Neptune may not be in VPC from GH Actions)
NEPTUNE_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 60 "${LAMBDA_URL}studio/api/neptune/health")
echo "GET /studio/api/neptune/health: $NEPTUNE_CODE"
# Verify CORS — single header only (ADR-056)
CORS_COUNT=$(curl -sv -H "Origin: https://graqle.com" --max-time 30 "${LAMBDA_URL}health" 2>&1 | grep -ci "access-control-allow-origin")
echo "CORS header count: $CORS_COUNT"
if [ "$CORS_COUNT" -gt 1 ]; then
echo "FAIL: Duplicate CORS headers detected ($CORS_COUNT)"
exit 1
fi