first
This commit is contained in:
Executable
+258
@@ -0,0 +1,258 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Seed source_file + code_smell artifacts per Code Analysis page."""
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
HEADERS = {
|
||||
"Content-Type": "application/json",
|
||||
"X-ASF-Tenant-Id": "t-demo-003",
|
||||
"X-ASF-User-Id": "u-mario",
|
||||
}
|
||||
BASE = "http://localhost:8000/api/v1"
|
||||
|
||||
|
||||
def req(method, path, body=None):
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
r = urllib.request.Request(
|
||||
BASE + path,
|
||||
data=data,
|
||||
headers=HEADERS,
|
||||
method=method,
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(r, timeout=15) as resp:
|
||||
return resp.status, json.loads(resp.read().decode() or "null")
|
||||
except urllib.error.HTTPError as e:
|
||||
txt = e.read().decode()
|
||||
try:
|
||||
payload = json.loads(txt)
|
||||
except Exception:
|
||||
payload = {"raw": txt}
|
||||
return e.code, payload
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 1. list projects
|
||||
code, projects = req("GET", "/projects?limit=10")
|
||||
assert code == 200, f"projects {code} {projects}"
|
||||
pid = projects[0]["id"]
|
||||
print(f"project_id={pid} name={projects[0]['name']}")
|
||||
|
||||
# 2. list existing to dedupe by external_id
|
||||
code, arts = req("GET", "/artifacts?limit=200")
|
||||
existing_external = {
|
||||
a["external_id"] for a in arts if a.get("external_id")
|
||||
}
|
||||
existing_types = {
|
||||
(a["type"], a["title"]) for a in arts
|
||||
}
|
||||
print(f"existing artifacts total: {len(arts)} ext_ids: {len(existing_external)} type/titles: {len(existing_types)}")
|
||||
|
||||
SOURCE_FILES = [
|
||||
{
|
||||
"external_id": "SRC-CORE-MW",
|
||||
"type": "source_file",
|
||||
"title": "asf-core/app/middleware/correlation.py",
|
||||
"status": "active",
|
||||
"source": "git",
|
||||
"content": (
|
||||
"\"\"\"Tenant & correlation middleware.\n\n"
|
||||
"Adds X-ASF-Correlation-Id and coerces X-ASF-Tenant-Id to UUID5.\n\"\"\"\n"
|
||||
"import uuid\nfrom fastapi import Request, Response\nfrom starlette.middleware.base import BaseHTTPMiddleware\n\n"
|
||||
"TENANT_NS = uuid.UUID('6ba7b812-9dad-11d1-80b4-00c04fd430c8')\n\n"
|
||||
"def _coerce_uuid(val):\n"
|
||||
" try:\n"
|
||||
" return uuid.UUID(val)\n"
|
||||
" except Exception:\n"
|
||||
" return uuid.uuid5(TENANT_NS, val or 'default')\n\n"
|
||||
"class CorrelationMiddleware(BaseHTTPMiddleware):\n"
|
||||
" async def dispatch(self, request: Request, call_next):\n"
|
||||
" cid = request.headers.get('X-ASF-Correlation-Id') or str(uuid.uuid4())\n"
|
||||
" request.state.correlation_id = cid\n"
|
||||
" tid = _coerce_uuid(request.headers.get('X-ASF-Tenant-Id'))\n"
|
||||
" request.state.tenant_id = tid\n"
|
||||
" response: Response = await call_next(request)\n"
|
||||
" response.headers['X-ASF-Correlation-Id'] = cid\n"
|
||||
" return response\n"
|
||||
),
|
||||
"metadata": {
|
||||
"language": "python",
|
||||
"path": "services/asf-core/app/middleware/correlation.py",
|
||||
"repo": "git@github.com:azurian/asf.git",
|
||||
"branch": "main",
|
||||
"loc": 36,
|
||||
"lines": [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],
|
||||
"commit_sha": "a1f22c33",
|
||||
},
|
||||
},
|
||||
{
|
||||
"external_id": "SRC-WEB-REQ",
|
||||
"type": "source_file",
|
||||
"title": "asf-web/app/requirements/page.tsx",
|
||||
"status": "active",
|
||||
"source": "git",
|
||||
"content": (
|
||||
"\"use client\";\n"
|
||||
"import React, { useMemo, useState } from \"react\";\n"
|
||||
"import { useQuery } from \"@tanstack/react-query\";\n"
|
||||
"import { ClipboardList, Search } from \"lucide-react\";\n"
|
||||
"import { artifacts, projects } from \"@/lib/api\";\n"
|
||||
"import { Card, CardBody, CardHeader, CardTitle } from \"@/components/ui/Card\";\n"
|
||||
"\n"
|
||||
"export default function RequirementsPage() {\n"
|
||||
" const [projectFilter, setProjectFilter] = useState('all');\n"
|
||||
" const [statusFilter, setStatusFilter] = useState('all');\n"
|
||||
" const projectsQ = useQuery({ queryKey: ['req-projects'], queryFn: () => projects.list({ limit: 50 }) });\n"
|
||||
" const listQ = useQuery({\n"
|
||||
" queryKey: ['req-list', projectFilter, statusFilter],\n"
|
||||
" queryFn: () => artifacts.list({\n"
|
||||
" status: statusFilter !== 'all' ? statusFilter : undefined,\n"
|
||||
" project_id: projectFilter !== 'all' ? projectFilter : undefined,\n"
|
||||
" type: 'requirement',\n"
|
||||
" limit: 200,\n"
|
||||
" }),\n"
|
||||
" });\n"
|
||||
" return (<Card>...</Card>);\n"
|
||||
"}\n"
|
||||
),
|
||||
"metadata": {
|
||||
"language": "typescript",
|
||||
"path": "frontend/asf-web/app/requirements/page.tsx",
|
||||
"repo": "git@github.com:azurian/asf.git",
|
||||
"branch": "main",
|
||||
"loc": 394,
|
||||
"framework": "nextjs14",
|
||||
"commit_sha": "a1f22c33",
|
||||
},
|
||||
},
|
||||
{
|
||||
"external_id": "SRC-WORKER-CB",
|
||||
"type": "source_file",
|
||||
"title": "asf-worker/app/worker/consumer.py",
|
||||
"status": "active",
|
||||
"source": "git",
|
||||
"content": (
|
||||
"\"\"\"RabbitMQ consumer factory + sync callbacks for DIRECT job queues.\"\"\"\n"
|
||||
"import asyncio\nimport structlog\nfrom aio_pika import IncomingMessage\n\n"
|
||||
"log = structlog.get_logger(__name__)\n\n"
|
||||
"def _make_consumer_callback(handler):\n"
|
||||
" def _cb(message: IncomingMessage):\n"
|
||||
" loop = asyncio.new_event_loop()\n"
|
||||
" try:\n"
|
||||
" loop.run_until_complete(handler(message))\n"
|
||||
" except Exception as exc:\n"
|
||||
" log.exception('consumer_handler_error', err=exc)\n"
|
||||
" finally:\n"
|
||||
" loop.close()\n"
|
||||
" return _cb\n"
|
||||
),
|
||||
"metadata": {
|
||||
"language": "python",
|
||||
"path": "services/asf-worker/app/worker/consumer.py",
|
||||
"repo": "git@github.com:azurian/asf.git",
|
||||
"branch": "main",
|
||||
"loc": 18,
|
||||
"commit_sha": "b4812a01",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
# code smells stored as type=ai_analysis with metadata.kind = "code_smell"
|
||||
# (to keep ArtifactType enum closed — no tests break)
|
||||
SMELLS = [
|
||||
{
|
||||
"external_id": "CS-001",
|
||||
"type": "ai_analysis",
|
||||
"title": "Bare except clause in CorrelationMiddleware.dispatch",
|
||||
"status": "open",
|
||||
"source": "ruff:EM102",
|
||||
"content": (
|
||||
"Rule Ruff E722 — bare `except Exception` without narrow clause may swallow "
|
||||
"cancellation errors unrelated to UUID parse. Prefer explicit ValueError/AttributeError "
|
||||
"or log.exception with correlation_id bound.\n"
|
||||
"Lines affected: 14-16."
|
||||
),
|
||||
"metadata": {
|
||||
"kind": "code_smell",
|
||||
"language": "python",
|
||||
"severity": "medium",
|
||||
"rule_id": "E722",
|
||||
"engine": "ruff 0.5.0",
|
||||
"path": "services/asf-core/app/middleware/correlation.py",
|
||||
"lines": [14, 15, 16],
|
||||
"fingerprint": "corr_mw_except_001",
|
||||
"confidence_pct": 98,
|
||||
},
|
||||
},
|
||||
{
|
||||
"external_id": "CS-002",
|
||||
"type": "ai_analysis",
|
||||
"title": "Missing N+1 query guard in requirements listQ (no select limit on projects)",
|
||||
"status": "open",
|
||||
"source": "eslint:perf-nplus1",
|
||||
"content": (
|
||||
"Requirements page fetches all 50 projects and maps via projectsQ even when a single project filter is selected. "
|
||||
"Consider deriving projectNameById lazily and paginating projects lookup.\n"
|
||||
"Affected page.tsx lines 78-89."
|
||||
),
|
||||
"metadata": {
|
||||
"kind": "code_smell",
|
||||
"language": "typescript",
|
||||
"severity": "low",
|
||||
"rule_id": "asf-perf-003",
|
||||
"engine": "eslint custom",
|
||||
"path": "frontend/asf-web/app/requirements/page.tsx",
|
||||
"lines": [78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89],
|
||||
"fingerprint": "req_list_nplus1_002",
|
||||
"confidence_pct": 75,
|
||||
},
|
||||
},
|
||||
{
|
||||
"external_id": "CS-003",
|
||||
"type": "ai_analysis",
|
||||
"title": "Coroutine leak risk: event loop not shared re-created per message",
|
||||
"status": "open",
|
||||
"source": "pytype:event-loop",
|
||||
"content": (
|
||||
"Worker consumer factory _cb spawns asyncio.new_event_loop() per IncomingMessage. "
|
||||
"Long-lived handlers under high volume may accumulate GC pressure from leftover loop objects. "
|
||||
"Recommendation: pass the shared running event loop to handler() via q.consume() and make handler truly async.\n"
|
||||
"Severity HIGH — affects every DIRECT job 24/7."
|
||||
),
|
||||
"metadata": {
|
||||
"kind": "code_smell",
|
||||
"language": "python",
|
||||
"severity": "high",
|
||||
"rule_id": "ASF-SEC-203",
|
||||
"engine": "pytype + manual review",
|
||||
"path": "services/asf-worker/app/worker/consumer.py",
|
||||
"lines": [8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
"fingerprint": "worker_loop_per_msg_003",
|
||||
"confidence_pct": 88,
|
||||
"cwe": "CWE-772",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
created = 0
|
||||
for body in SOURCE_FILES + SMELLS:
|
||||
if (body["type"], body["title"]) in existing_types or body.get("external_id") in existing_external:
|
||||
print(f" SKIP exists ext_id={body.get('external_id')} type={body['type']} title={body['title'][:60]}")
|
||||
continue
|
||||
body_full = {"project_id": pid, **body}
|
||||
code, resp = req("POST", "/artifacts", body_full)
|
||||
if code in (201, 200):
|
||||
created += 1
|
||||
print(f" OK [{code}] type={body['type']} id={str(resp['id'])[:8]} title={body['title'][:60]}")
|
||||
else:
|
||||
print(f" FAIL [{code}] type={body['type']} body={resp}")
|
||||
|
||||
print(f"\nDONE: created={created} new code artifacts")
|
||||
|
||||
# print summary counts per type
|
||||
code, arts2 = req("GET", "/artifacts?limit=200")
|
||||
from collections import Counter
|
||||
cnt = Counter(a["type"] for a in arts2)
|
||||
for t, c in cnt.items():
|
||||
print(f" count {t}: {c}")
|
||||
Executable
+197
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# seed-demo.sh — Popola ASF con i dati DEMO del tenant t-demo-003.
|
||||
#
|
||||
# Sequenza (idempotente dal lato API - POST ritorna 201 per nuovi artifact,
|
||||
# ma se seed precedenti esistono già verrà creato duplicato perché non abbiamo
|
||||
# un controllo unique ext_id in questa fase MVP. Per sicurezza cancella TUTTI
|
||||
# artifacts + jobs prima di ricreare quando si specifica --clean.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/seed-demo.sh # seeds normali (3 blocchi trace + code + extra)
|
||||
# ./scripts/seed-demo.sh --clean # PURGA artifacts + jobs t-demo-003 POI seed
|
||||
# ./scripts/seed-demo.sh --jobs # + 5 job echo demo in coda (devops/testing/...)
|
||||
#
|
||||
# Richiede:
|
||||
# • ASF Core attivo su http://localhost:8000 (scripts/start-stack.sh)
|
||||
# • Python .venv attivo o eseguibile in /root/ASF/.venv/bin/python
|
||||
# • Worker attivo se usi --jobs (altrimenti jobs stanno in QUEUED)
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PY="$ROOT/.venv/bin/python"
|
||||
CORE_URL="${ASF_CORE_URL:-http://localhost:8000}"
|
||||
TENANT="${X_ASF_TENANT_ID:-t-demo-003}"
|
||||
USER="${X_ASF_USER_ID:-u-mario}"
|
||||
|
||||
CLEAN=0
|
||||
JOBS=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--clean) CLEAN=1 ;;
|
||||
--jobs) JOBS=1 ;;
|
||||
-h|--help)
|
||||
sed -n '2,26p' "$0"
|
||||
exit 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
log() { printf " \033[1;34m→\033[0m %s\n" "$*"; }
|
||||
ok() { printf " \033[1;32m✔\033[0m %s\n" "$*"; }
|
||||
fail() { printf " \033[1;31m✘\033[0m %s\n" "$*" >&2; exit 1; }
|
||||
|
||||
curl_h() {
|
||||
# $1 = method, $2 = path, stdin optional body. Prints status + JSON body.
|
||||
local m="$1" p="$2"
|
||||
shift 2
|
||||
curl -s -X "$m" -H "X-ASF-Tenant-Id: $TENANT" -H "X-ASF-User-Id: $USER" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${CORE_URL}/api/v1${p}" "$@"
|
||||
}
|
||||
|
||||
# --- Sanity check: Core UP ---
|
||||
log "ASF Core health check su $CORE_URL"
|
||||
H=$(curl -s -o /dev/null -w "%{http_code}" "${CORE_URL}/api/v1/health" || true)
|
||||
if [[ "$H" != "200" ]]; then
|
||||
fail "ASF Core non risponde (HTTP $H). Avvia con: bash scripts/start-stack.sh"
|
||||
fi
|
||||
ok "ASF Core UP (health=$H)"
|
||||
|
||||
# --- Cleanup (opzionale) ---
|
||||
if [[ $CLEAN == 1 ]]; then
|
||||
log "Cleanup artifacts + jobs per tenant $TENANT (DELETE /artifacts e /jobs listati)"
|
||||
|
||||
# MVP: asf-core non espone DELETE /artifacts bulk. Useremo GET lista + DELETE /artifacts/{id}.
|
||||
A_IDS=$(curl_h GET "/artifacts?limit=500" | "$PY" -c '
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
for a in d: print(a["id"])
|
||||
')
|
||||
N_A=$(echo -n "$A_IDS" | grep -c . || true)
|
||||
log " DELETE $N_A artifacts..."
|
||||
if [[ -n "$A_IDS" ]]; then
|
||||
while read -r id; do
|
||||
curl -s -o /dev/null -X DELETE \
|
||||
-H "X-ASF-Tenant-Id: $TENANT" -H "X-ASF-User-Id: $USER" \
|
||||
"${CORE_URL}/api/v1/artifacts/${id}" || true
|
||||
done <<< "$A_IDS"
|
||||
fi
|
||||
|
||||
J_IDS=$(curl_h GET "/jobs?limit=500" | "$PY" -c '
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
for j in d: print(j["id"])
|
||||
' 2>/dev/null || true)
|
||||
N_J=$(echo -n "$J_IDS" | grep -c . || true)
|
||||
log " PATCH CANCEL $N_J jobs..."
|
||||
if [[ -n "$J_IDS" ]]; then
|
||||
while read -r id; do
|
||||
curl -s -o /dev/null -X PATCH \
|
||||
-H "X-ASF-Tenant-Id: $TENANT" -H "X-ASF-User-Id: $USER" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"status":"cancelled"}' \
|
||||
"${CORE_URL}/api/v1/jobs/${id}" || true
|
||||
done <<< "$J_IDS"
|
||||
fi
|
||||
ok "Cleanup completato"
|
||||
fi
|
||||
|
||||
# --- Seeds ---
|
||||
log "Seed 1/4: Traceability artifacts (REQ → TASK → PR → TEST/BUILD → DEPLOY)"
|
||||
"$PY" "$ROOT/scripts/seed-traceability.py" >/tmp/seed1.log 2>&1 \
|
||||
|| fail "seed-traceability fallito. Log: /tmp/seed1.log"
|
||||
ok " OK ($(wc -l < /tmp/seed1.log) righe log)"
|
||||
|
||||
log "Seed 2/4: Relazioni traceability (5 edges: implements/verifies/deployed_as/generated_from)"
|
||||
"$PY" "$ROOT/scripts/seed-rels-only.py" >/tmp/seed2.log 2>&1 \
|
||||
|| fail "seed-rels fallito. Log: /tmp/seed2.log"
|
||||
ok " OK ($(wc -l < /tmp/seed2.log) righe log)"
|
||||
|
||||
log "Seed 3/4: Source files + Code smells (3 files, 3 smells)"
|
||||
"$PY" "$ROOT/scripts/seed-code-artifacts.py" >/tmp/seed3.log 2>&1 \
|
||||
|| fail "seed-code-artifacts fallito. Log: /tmp/seed3.log"
|
||||
ok " OK ($(wc -l < /tmp/seed3.log) righe log)"
|
||||
|
||||
log "Seed 4/4: Extra DOC / COMMIT / SEC / INC + 4 relazioni (densify graph)"
|
||||
"$PY" "$ROOT/scripts/seed-extra-rels.py" >/tmp/seed4.log 2>&1 \
|
||||
|| fail "seed-extra-rels fallito. Log: /tmp/seed4.log"
|
||||
ok " OK ($(wc -l < /tmp/seed4.log) righe log)"
|
||||
|
||||
# --- Jobs demo (opzionale) ---
|
||||
if [[ $JOBS == 1 ]]; then
|
||||
log "Enqueue 5 demo jobs (echo handler DEVOPS/TESTING/...)"
|
||||
OUT=$(curl -s -X POST \
|
||||
-H "X-ASF-Tenant-Id: $TENANT" -H "X-ASF-User-Id: $USER" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-raw '{
|
||||
"jobs": [
|
||||
{"type":"devops","priority":"normal","payload":{"name":"deploy-dev","env":"dev"}},
|
||||
{"type":"testing","priority":"high","payload":{"name":"smoke-tests","framework":"pytest"}},
|
||||
{"type":"devops","priority":"normal","payload":{"name":"build-image","image":"asf/app:latest"}},
|
||||
{"type":"notifications","priority":"low","payload":{"channel":"email","to":"admins"}},
|
||||
{"type":"indexing","priority":"normal","payload":{"source":"github/asf","branch":"main"}}
|
||||
]
|
||||
}' "${CORE_URL}/api/v1/jobs/batch" 2>&1 || echo "fallback")
|
||||
# Fallback per /batch non esistente (MVP usa singolo POST per job)
|
||||
if [[ "$OUT" == *"fallback"* || "$OUT" == *"Not Found"* || "$OUT" == *"404"* || "$OUT" == *"405"* ]]; then
|
||||
log " /batch non disponibile → 5 POST singoli:"
|
||||
TYPES=("devops" "testing" "devops" "notifications" "indexing")
|
||||
PAYLOADS=(
|
||||
'{"name":"deploy-dev","env":"dev"}'
|
||||
'{"name":"smoke-tests","framework":"pytest"}'
|
||||
'{"name":"build-image","image":"asf/app:latest"}'
|
||||
'{"channel":"email","to":"admins"}'
|
||||
'{"source":"github/asf","branch":"main"}'
|
||||
)
|
||||
PRIO=("normal" "high" "normal" "low" "normal")
|
||||
for i in 0 1 2 3 4; do
|
||||
JS=$(curl -s -X POST \
|
||||
-H "X-ASF-Tenant-Id: $TENANT" -H "X-ASF-User-Id: $USER" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"type\":\"${TYPES[$i]}\",\"priority\":\"${PRIO[$i]}\",\"payload\":${PAYLOADS[$i]}}" \
|
||||
"${CORE_URL}/api/v1/jobs/submit")
|
||||
echo -n "$JS" | "$PY" -c 'import json,sys; j=json.load(sys.stdin); print(" - job id=", j.get("id","?")[:8]+"…", "accepted=", j.get("accepted"))' 2>/dev/null
|
||||
done
|
||||
else
|
||||
ok " /batch accettato"
|
||||
fi
|
||||
ok "5 demo jobs accodati (avvia asf-worker per completarli)"
|
||||
fi
|
||||
|
||||
# --- Report finale ---
|
||||
log "Report finale tenant $TENANT:"
|
||||
echo ""
|
||||
curl_h GET "/artifacts?limit=500" | "$PY" - <<'PY'
|
||||
import json,sys
|
||||
from collections import Counter
|
||||
d=json.load(sys.stdin)
|
||||
c=Counter(a.get("type","?") for a in d)
|
||||
print(" Artifacts totali:", len(d))
|
||||
for k,v in sorted(c.items(), key=lambda x:-x[1]):
|
||||
print(f" - {k:18s} {v}")
|
||||
PY
|
||||
echo ""
|
||||
curl_h GET "/traceability/graph" 2>/dev/null | "$PY" -c '
|
||||
import json,sys
|
||||
g=json.load(sys.stdin)
|
||||
print(f" Traceability graph: nodes={len(g.get(\"nodes\",[]))} edges={len(g.get(\"edges\",[]))}")
|
||||
' 2>/dev/null || true
|
||||
echo ""
|
||||
curl_h GET "/jobs" 2>/dev/null | "$PY" - <<'PY'
|
||||
import json,sys
|
||||
from collections import Counter
|
||||
d=json.load(sys.stdin)
|
||||
c=Counter(j.get("status","?") for j in d)
|
||||
print(" Jobs totali:", len(d))
|
||||
for k,v in sorted(c.items(), key=lambda x:-x[1]):
|
||||
print(f" - {k:10s} {v}")
|
||||
PY
|
||||
echo ""
|
||||
ok "SEED DEMO completato."
|
||||
echo ""
|
||||
echo " Suggerimenti:"
|
||||
echo " • Apri http://localhost:3000 per vedere UI (hard refresh Ctrl+Shift+R se serve)"
|
||||
echo " • bash scripts/status-stack.sh per stato core/web/worker"
|
||||
echo " • bash scripts/seed-demo.sh --clean --jobs per ricreare TUTTO da zero"
|
||||
Executable
+165
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
"""POST extra artifacts + 4 relazioni per densificare Traceability + Context Explorer.
|
||||
|
||||
Creati in tenant t-demo-003 / unico project esistente:
|
||||
1. DOC "Product Requirements.md" (DOCUMENT) --references--> REQ-103
|
||||
2. COMMIT "0abc123 feat: REQ-103" (CODE_COMMIT) --generated_from--> PR#88
|
||||
3. SEC "SEC-025 SQL injection input sanitization" (SECURITY_FINDING) --mitigated_by--> PR#88
|
||||
4. INC "INC-77 Login timeout" (INCIDENT) --caused_by--> DEP-44 deployment
|
||||
|
||||
Rileva artifact IDs esistenti via LIST + ext_id exact match.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
|
||||
BASE = "http://localhost:8000/api/v1"
|
||||
H = {
|
||||
"X-ASF-Tenant-Id": "t-demo-003",
|
||||
"X-ASF-User-Id": "u-mario",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
LOOKUP = {
|
||||
"REQ": "REQ-103",
|
||||
"PR": "PR#88",
|
||||
"DEP": "DEP-44",
|
||||
}
|
||||
|
||||
|
||||
def req(method: str, path: str, body=None) -> tuple[int, object]:
|
||||
data = None
|
||||
if body is not None:
|
||||
data = json.dumps(body).encode()
|
||||
r = urllib.request.Request(BASE + path, data=data, method=method, headers=H)
|
||||
try:
|
||||
with urllib.request.urlopen(r, timeout=10) as resp:
|
||||
text = resp.read().decode()
|
||||
return resp.status, json.loads(text) if text else None
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode()
|
||||
print(f" !! {method} {path} -> {e.code}: {body[:300]}", file=sys.stderr)
|
||||
return e.code, body
|
||||
|
||||
|
||||
def list_artifacts() -> list[dict]:
|
||||
s, d = req("GET", "/artifacts?limit=500")
|
||||
assert s == 200 and isinstance(d, list), f"list artifacts failed: {s}"
|
||||
return d
|
||||
|
||||
|
||||
def find_project_id(arts: list[dict]) -> str:
|
||||
"""Unico project del tenant: take artifact project_id non-None first."""
|
||||
for a in arts:
|
||||
if a.get("project_id"):
|
||||
return a["project_id"]
|
||||
s, pl = req("GET", "/projects?limit=50")
|
||||
assert s == 200 and isinstance(pl, list) and pl
|
||||
return pl[0]["id"]
|
||||
|
||||
|
||||
def create_artifact(payload: dict) -> dict:
|
||||
s, d = req("POST", "/artifacts", payload)
|
||||
if s != 201 or not isinstance(d, dict):
|
||||
raise RuntimeError(f"CREATE {payload.get('type')} {payload.get('external_id')} HTTP {s}")
|
||||
return d
|
||||
|
||||
|
||||
def create_rel(src: str, tgt: str, rel_type: str) -> int:
|
||||
body = {
|
||||
"source_artifact_id": src,
|
||||
"target_artifact_id": tgt,
|
||||
"relationship_type": rel_type,
|
||||
}
|
||||
s, _ = req("POST", "/relationships", body)
|
||||
return s
|
||||
|
||||
|
||||
def main() -> int:
|
||||
arts = list_artifacts()
|
||||
existing: dict[str, str] = {}
|
||||
for k, ext in LOOKUP.items():
|
||||
match = next((a for a in arts if a.get("external_id") == ext), None)
|
||||
if match is None:
|
||||
print(f" !! missing {ext}, aborting.")
|
||||
return 2
|
||||
existing[k] = match["id"]
|
||||
print(f" {k:3s} {ext!r:12s} -> {match['id'][:10]}…")
|
||||
|
||||
pid = find_project_id(arts)
|
||||
print(f" project id = {pid[:10]}…")
|
||||
|
||||
print("\n== Creating 4 extra artifacts ==")
|
||||
extra = [
|
||||
{
|
||||
"project_id": pid,
|
||||
"type": "document",
|
||||
"external_id": "DOC-PRD-2026",
|
||||
"title": "Product Requirements v2.3",
|
||||
"status": "active",
|
||||
"content": "# Product Requirements 2.3\n\nLiving PRD covering authentication, billing and reporting.\n\n## Goals\nProvide clear tenancy and billing.\n",
|
||||
"metadata": {"format": "markdown", "page_count": 18, "owner": "pm"},
|
||||
},
|
||||
{
|
||||
"project_id": pid,
|
||||
"type": "commit",
|
||||
"external_id": "COMMIT-0abc123",
|
||||
"title": "feat(auth): implement REQ-103 SSO flow",
|
||||
"content": "commit 0abc123df\nAuthor: mario\nDate: today\n\nAdds Okta SAML callback + session refresh route.\n",
|
||||
"metadata": {"sha": "0abc123df", "author": "mario@asf.test", "repo": "asf/mono", "branch": "main"},
|
||||
},
|
||||
{
|
||||
"project_id": pid,
|
||||
"type": "security_finding",
|
||||
"external_id": "SEC-025",
|
||||
"title": "SQL injection risk in profile search (low confidence)",
|
||||
"status": "open",
|
||||
"content": "Input param q passed directly to $regex without sanitization. Scanner: semgrep. Confidence 82%.",
|
||||
"metadata": {"cwe": "CWE-89", "severity": "high", "scanner": "semgrep", "confidence_pct": 82},
|
||||
},
|
||||
{
|
||||
"project_id": pid,
|
||||
"type": "incident",
|
||||
"external_id": "INC-77",
|
||||
"title": "Login timeout spike 05:00-05:14 UTC",
|
||||
"status": "mitigated",
|
||||
"content": "Session refresh endpoint returned 5xx for 4% of users. MTTR 14 minutes. 27 customers impacted.",
|
||||
"metadata": {"severity": "sev2", "mttr_min": 14, "customers_impacted": 27},
|
||||
},
|
||||
]
|
||||
ids: list[str] = []
|
||||
for pl in extra:
|
||||
d = create_artifact(pl)
|
||||
print(f" [201] {pl['type']:15s} {pl['external_id']:18s} id={d['id'][:8]}…")
|
||||
ids.append(d["id"])
|
||||
|
||||
doc_id, commit_id, sec_id, inc_id = ids
|
||||
print("\n== Creating 4 extra relationships ==")
|
||||
edges = [
|
||||
(doc_id, existing["REQ"], "documented_by", "DOC --documented_by--> REQ"),
|
||||
(commit_id, existing["PR"], "generated_from", "COMMIT --generated_from--> PR"),
|
||||
(sec_id, existing["PR"], "related_to", "SEC --related_to--> PR"),
|
||||
(inc_id, existing["DEP"], "caused_by", "INC --caused_by--> DEP"),
|
||||
]
|
||||
all_ok = True
|
||||
for src, tgt, rt, label in edges:
|
||||
s = create_rel(src, tgt, rt)
|
||||
chk = "OK" if s == 201 else "FAIL"
|
||||
print(f" [{s:3d}] {chk:4s} {label}")
|
||||
if s != 201:
|
||||
all_ok = False
|
||||
|
||||
time.sleep(0.3)
|
||||
s, g = req("GET", f"/traceability/graph?project_id={pid}")
|
||||
if s == 200 and isinstance(g, dict):
|
||||
print(f"\n== Traceability graph now: nodes={len(g.get('nodes', []))} edges={len(g.get('edges', []))} ==")
|
||||
return 0 if all_ok else 5
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Retry 5 POST relationships SENZA campo `meta` (was extra_forbidden 422). Chained graph DEP→BUILD→PR→TASK→REQ + TEST→PR."""
|
||||
from __future__ import annotations
|
||||
import json, subprocess, sys
|
||||
BASE = "http://127.0.0.1:8000/api/v1"
|
||||
H = ["-H","X-ASF-Tenant-Id: t-demo-003","-H","X-ASF-User-Id: u-mario","-H","Content-Type: application/json"]
|
||||
|
||||
def curl(method, path, body=None):
|
||||
cmd = ["curl","-s","-m","15","-o","-","-w","\n%{http_code}","-X",method] + H
|
||||
if body is not None: cmd += ["--data", json.dumps(body, ensure_ascii=False)]
|
||||
cmd.append(BASE+path)
|
||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
out = (r.stdout or "{}").strip()
|
||||
if "\n" in out:
|
||||
jp, cp = out.rsplit("\n", 1)
|
||||
else:
|
||||
jp, cp = "", out
|
||||
try: code = int(cp)
|
||||
except: code = 0
|
||||
try: data = json.loads(jp or "null")
|
||||
except Exception as exc: data = f"ERR {exc}: {jp[:200]!r}"
|
||||
print(f" [{method:6s}{path:22s}] HTTP={code:3d}",
|
||||
f"id={str(data.get('id',''))[:10] if isinstance(data,dict) else '?'}",
|
||||
"rel=", (data.get("source_artifact_id","")[:8] if isinstance(data,dict) else "?"),
|
||||
"→",(data.get("target_artifact_id","")[:8] if isinstance(data,dict) else "?"),
|
||||
"type=",(data.get("relationship_type","?") if isinstance(data,dict) else "?"),
|
||||
file=sys.stderr)
|
||||
return code, data
|
||||
|
||||
def main():
|
||||
_, arts = curl("GET", "/artifacts?limit=50")
|
||||
assert isinstance(arts, list)
|
||||
by_type = {}
|
||||
for a in arts:
|
||||
if isinstance(a,dict):
|
||||
by_type.setdefault(a["type"], []).append(a)
|
||||
req = by_type["requirement"][0]
|
||||
task = by_type["task"][0]
|
||||
pr = by_type["pull_request"][0]
|
||||
test = by_type["test"][0]
|
||||
build = by_type["build"][0]
|
||||
deploy= by_type["deployment"][0]
|
||||
print(f" req={str(req['id'])[:10]} task={str(task['id'])[:10]} pr={str(pr['id'])[:10]} test={str(test['id'])[:10]} build={str(build['id'])[:10]} deploy={str(deploy['id'])[:10]}", file=sys.stderr)
|
||||
rels = [
|
||||
(task["id"], req["id"], "implements"),
|
||||
(pr["id"], task["id"], "generated_from"),
|
||||
(build["id"], pr["id"], "generated_from"),
|
||||
(test["id"], pr["id"], "verifies"),
|
||||
(deploy["id"], build["id"], "deployed_as"),
|
||||
]
|
||||
oks=0
|
||||
for s,t,k in rels:
|
||||
code, d = curl("POST","/relationships", {"source_artifact_id": s, "target_artifact_id": t, "relationship_type": k})
|
||||
if 200 <= code < 300: oks += 1
|
||||
print(f"\n rels OK = {oks}/5", file=sys.stderr)
|
||||
|
||||
code, graph = curl("GET", "/traceability/graph?limit=200")
|
||||
assert code == 200 and isinstance(graph, dict)
|
||||
nodes = graph.get("nodes", []); edges = graph.get("edges", [])
|
||||
print(json.dumps({"nodes":len(nodes),"edges":len(edges),
|
||||
"node_types":[n.get("type") for n in nodes],
|
||||
"edge_labels":[e.get("label") for e in edges]}))
|
||||
return 0 if oks == 5 else 2
|
||||
|
||||
raise SystemExit(main())
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Seed tenant t-demo-003 with 5 artifacts + 5 relationships for a rich Traceability Graph demo.
|
||||
|
||||
Endpoint testati via curl:
|
||||
- POST /api/v1/artifacts
|
||||
- POST /api/v1/relationships
|
||||
- GET /api/v1/traceability/graph
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
BASE = "http://127.0.0.1:8000/api/v1"
|
||||
H = [
|
||||
"-H", "X-ASF-Tenant-Id: t-demo-003",
|
||||
"-H", "X-ASF-User-Id: u-mario",
|
||||
"-H", "Content-Type: application/json",
|
||||
]
|
||||
|
||||
|
||||
def curl(method: str, path: str, body: dict[str, Any] | None = None) -> tuple[int, dict[str, Any] | list | str]:
|
||||
cmd = ["curl", "-s", "-m", "15", "-o", "-", "-w", "\n%{http_code}", "-X", method] + H
|
||||
if body is not None:
|
||||
cmd += ["--data", json.dumps(body, ensure_ascii=False)]
|
||||
cmd.append(f"{BASE}{path}")
|
||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
out_raw = r.stdout.strip() or "{}"
|
||||
# Last line is the http code we injected with -w
|
||||
if "\n" in out_raw:
|
||||
json_part, code_part = out_raw.rsplit("\n", 1)
|
||||
else:
|
||||
json_part, code_part = "", out_raw
|
||||
try:
|
||||
code = int(code_part)
|
||||
except ValueError:
|
||||
code = 0
|
||||
try:
|
||||
data = json.loads(json_part or "null")
|
||||
except Exception as exc: # pragma: no cover
|
||||
data = f"JSON_ERR {exc} RAW={json_part[:200]!r}"
|
||||
print(f" [{method:6s} {path:24s}] HTTP={code:3d}",
|
||||
f"type={data.get('type','?') if isinstance(data,dict) else '?'}\tid={str(data.get('id',''))[:10] if isinstance(data,dict) else '?'}",
|
||||
file=sys.stderr)
|
||||
return code, data
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("# Step 1: list existing artifacts + project_id", file=sys.stderr)
|
||||
_, arts = curl("GET", "/artifacts?limit=50")
|
||||
assert isinstance(arts, list), f"artifacts list not a list: {arts!r}"
|
||||
req_103 = None
|
||||
project_id = None
|
||||
for a in arts:
|
||||
if isinstance(a, dict) and a.get("external_id") == "REQ-103":
|
||||
req_103 = a
|
||||
project_id = a.get("project_id")
|
||||
break
|
||||
if not req_103:
|
||||
# fallback: first artifact
|
||||
req_103 = arts[0] if arts else None
|
||||
project_id = req_103.get("project_id") if isinstance(req_103, dict) else None
|
||||
print(f" req_103 id={str(req_103['id'])[:10] if isinstance(req_103,dict) else None} project_id={str(project_id)[:10] if project_id else None}",
|
||||
file=sys.stderr)
|
||||
|
||||
def make(type_: str, ext: str, title: str, content: str) -> dict[str, Any]:
|
||||
code, data = curl(
|
||||
"POST", "/artifacts",
|
||||
{
|
||||
"type": type_,
|
||||
"external_id": ext,
|
||||
"title": title,
|
||||
"content": content,
|
||||
"status": "done",
|
||||
"project_id": project_id,
|
||||
},
|
||||
)
|
||||
assert code == 201 and isinstance(data, dict) and "id" in data, f"make {type_} FAILED HTTP={code} {data}"
|
||||
return data
|
||||
|
||||
print(file=sys.stderr); print("# Step 2: create 5 extra artifacts", file=sys.stderr)
|
||||
task = make("task", "TASK-101", "Implementa tenant trust header coerce", "FastAPI correlation middleware: UUID UUID5 per stringhe t-demo-xxx, no 400 invalid")
|
||||
pr = make("pull_request", "PR#88", "feat(rbac): tenant trust + policies v1", "feat(rbac): add trust header middleware + policy service helpers")
|
||||
test = make("test", "TEST-22", "cross-tenant reads/writes blocked", "Pytest suite proving cross-tenant artifact operations return 404/403")
|
||||
build = make("build", "BUILD-910","asf-core build #910 (main)", "GitHub Actions build main branch, all checks green.")
|
||||
deploy = make("deployment", "DEP-44", "asf-core v0.5.0 → prod-west-1", "ArgoCD sync asf-core to cluster prod-west-1 eu-west")
|
||||
|
||||
print(file=sys.stderr); print("# Step 3: create 5 relationships (chain: DEP → BUILD → PR → TASK → REQ, TEST → PR)", file=sys.stderr)
|
||||
rels_pairs = [
|
||||
(task, req_103, "implements"),
|
||||
(pr, task, "generated_from"),
|
||||
(build, pr, "generated_from"),
|
||||
(test, pr, "verifies"),
|
||||
(deploy, build, "deployed_as"),
|
||||
]
|
||||
for src, tgt, kind in rels_pairs:
|
||||
code, rel = curl(
|
||||
"POST", "/relationships",
|
||||
{
|
||||
"source_artifact_id": src["id"],
|
||||
"target_artifact_id": tgt["id"],
|
||||
"relationship_type": kind,
|
||||
"meta": {"seed": "traceability_demo"},
|
||||
},
|
||||
)
|
||||
assert code < 300 and isinstance(rel, dict) and "id" in rel, f"REL FAIL {kind} -> {rel}"
|
||||
|
||||
print(file=sys.stderr); print("# Step 4: GET traceability graph → counts", file=sys.stderr)
|
||||
code, graph = curl("GET", "/traceability/graph?limit=200")
|
||||
assert code == 200 and isinstance(graph, dict), f"GRAPH FAIL HTTP={code} {graph}"
|
||||
nodes = graph.get("nodes", [])
|
||||
edges = graph.get("edges", [])
|
||||
print(f"\n=== SEED SUCCESS ===", file=sys.stderr)
|
||||
print(f" graph.nodes = {len(nodes)} graph.edges = {len(edges)}", file=sys.stderr)
|
||||
# print to stdout for bash pipeline capture (JSON):
|
||||
print(json.dumps({"nodes": len(nodes), "edges": len(edges),
|
||||
"node_ids": [{"id":str(n["id"])[:10],"type":n.get("type"),"label":n.get("label")} for n in nodes],
|
||||
"edge_labels": [{"s":e["source"][:8],"t":e["target"][:8],"lbl":e.get("label")} for e in edges]}))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+225
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env bash
|
||||
# start-stack.sh — avvia ASF Core, ASF Web (Next.js), e opzionalmente ASF Worker.
|
||||
# Usage:
|
||||
# ./scripts/start-stack.sh # core + web, reload + dev mode
|
||||
# ./scripts/start-stack.sh --no-reload # core senza --reload (productionish)
|
||||
# ./scripts/start-stack.sh --with-worker # avvia anche asf-worker (7 code)
|
||||
# ./scripts/start-stack.sh --web-only # solo frontend Next.js
|
||||
# ./scripts/start-stack.sh --core-only # solo backend Core (FastAPI)
|
||||
# Bindings default: 0.0.0.0 (core 8000, web 3000).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
usage() {
|
||||
sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit 0
|
||||
}
|
||||
[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && usage
|
||||
|
||||
WITH_WORKER=0
|
||||
RELOAD=1
|
||||
CORE=1
|
||||
WEB=1
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--with-worker|-w) WITH_WORKER=1 ;;
|
||||
--no-reload) RELOAD=0 ;;
|
||||
--web-only) CORE=0; WITH_WORKER=0 ;;
|
||||
--core-only) WEB=0 ;;
|
||||
-h|--help) usage ;;
|
||||
*) echo "Unknown option: $arg"; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 1) Preflight checks
|
||||
# ------------------------------------------------------------
|
||||
VENV_DIR="$ROOT_DIR/.venv"
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
echo "[start-stack] ❌ .venv non trovata in $VENV_DIR. Crea prima la venv: python -m venv .venv && source .venv/bin/activate" >&2
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
ENV_FILE="$ROOT_DIR/.env"
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "[start-stack] ⚠️ .env non trovato, uso .env.example (se esiste)."
|
||||
if [ -f "$ROOT_DIR/.env.example" ]; then
|
||||
cp "$ROOT_DIR/.env.example" "$ROOT_DIR/.env"
|
||||
fi
|
||||
fi
|
||||
# shellcheck disable=SC1090
|
||||
set -a; [ -f "$ENV_FILE" ] && source "$ENV_FILE"; set +a
|
||||
|
||||
export ASF_ROOT="$ROOT_DIR"
|
||||
export PYTHONPATH="\
|
||||
$ROOT_DIR/packages/asf-domain:\
|
||||
$ROOT_DIR/services/asf-events:\
|
||||
$ROOT_DIR/services/asf-connectors:\
|
||||
$ROOT_DIR/services/asf-core:\
|
||||
$ROOT_DIR/services/asf-worker:\
|
||||
$ROOT_DIR/services/asf-ai:\
|
||||
$ROOT_DIR/services/asf-context\
|
||||
${PYTHONPATH:+:$PYTHONPATH}"
|
||||
|
||||
export NEXT_PUBLIC_ASF_CORE_URL="${NEXT_PUBLIC_ASF_CORE_URL:-http://localhost:8000}"
|
||||
export NEXT_PUBLIC_DEV_TENANT_ID="${NEXT_PUBLIC_DEV_TENANT_ID:-t-demo-001}"
|
||||
export NEXT_PUBLIC_DEV_USER_ID="${NEXT_PUBLIC_DEV_USER_ID:-u-admin}"
|
||||
|
||||
LOG_DIR="$ROOT_DIR/var/log/asf"
|
||||
PID_DIR="$ROOT_DIR/var/run"
|
||||
mkdir -p "$LOG_DIR" "$PID_DIR"
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 2) Helpers
|
||||
# ------------------------------------------------------------
|
||||
pids=()
|
||||
names=()
|
||||
|
||||
start_in_dir() {
|
||||
local name="$1"; shift
|
||||
local dir="$1"; shift
|
||||
local log="$LOG_DIR/$name.log"
|
||||
echo " starting $name ($(basename "$dir")) ..."
|
||||
(
|
||||
cd "$dir"
|
||||
# shellcheck disable=SC2093
|
||||
exec "$@" >"$log" 2>&1
|
||||
) &
|
||||
local pid=$!
|
||||
pids+=("$pid")
|
||||
names+=("$name")
|
||||
echo " pid=$pid log=$log"
|
||||
}
|
||||
|
||||
print_header() {
|
||||
local COLS
|
||||
COLS=$(tput cols 2>/dev/null || echo 80)
|
||||
printf '═%.0s' $(seq 1 "$COLS"); echo
|
||||
printf " ASF local stack · %s · tenant=%s user=%s\n" \
|
||||
"$(date '+%F %T %Z')" \
|
||||
"$NEXT_PUBLIC_DEV_TENANT_ID" \
|
||||
"$NEXT_PUBLIC_DEV_USER_ID"
|
||||
printf '═%.0s' $(seq 1 "$COLS"); echo
|
||||
}
|
||||
|
||||
graceful_stop() {
|
||||
echo
|
||||
echo "[start-stack] SIGINT/SIGTERM received — stopping ${#pids[@]} processes (${names[*]})."
|
||||
for i in "${!pids[@]}"; do
|
||||
local pid="${pids[$i]}"
|
||||
local name="${names[$i]}"
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo " stopping $name (pid=$pid)"
|
||||
kill -TERM "$pid" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
# wait them out
|
||||
local waited=0
|
||||
while [ $waited -lt 10 ]; do
|
||||
local alive=0
|
||||
for pid in "${pids[@]}"; do kill -0 "$pid" 2>/dev/null && alive=1; done
|
||||
[ $alive -eq 0 ] && break
|
||||
sleep 1; waited=$((waited+1))
|
||||
done
|
||||
# force kill any stragglers
|
||||
for pid in "${pids[@]}"; do
|
||||
kill -0 "$pid" 2>/dev/null && { echo " force kill pid=$pid"; kill -9 "$pid" 2>/dev/null || true; }
|
||||
done
|
||||
echo "[start-stack] stopped."
|
||||
exit 0
|
||||
}
|
||||
trap graceful_stop INT TERM
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 3) Dependency quick liveness (Mongo + Rabbit — best effort)
|
||||
# ------------------------------------------------------------
|
||||
print_header
|
||||
echo "[start-stack] python=$(python -V 2>&1 | cut -d' ' -f2) node=$(node -v) npm=v$(npm -v 2>/dev/null)"
|
||||
echo "[start-stack] PYTHONPATH[0:80]=${PYTHONPATH:0:80}…"
|
||||
echo
|
||||
echo "[deps] Mongo / Rabbit best-effort reachability (5s timeout)…"
|
||||
python - <<PY 2>/dev/null || true
|
||||
import os,socket,time
|
||||
for host,port,name in [("172.19.40.102",27017,"Mongo rs0 PRIMARY"),("172.19.40.103",27017,"Mongo rs0 SEED"),("172.19.40.201",5672,"RabbitMQ AMQP"),("172.19.40.201",15672,"RabbitMQ HTTP mgmt")]:
|
||||
s=socket.socket(); s.settimeout(2)
|
||||
try:
|
||||
s.connect((host,port)); print(f" ✅ {name:22s} {host}:{port} reachable")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ {name:22s} {host}:{port} unreachable ({e})")
|
||||
finally: s.close()
|
||||
PY
|
||||
echo
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 4) Start processes
|
||||
# ------------------------------------------------------------
|
||||
echo "[start-stack] launching services (bind 0.0.0.0)…"
|
||||
|
||||
# Core (FastAPI)
|
||||
if [ $CORE -eq 1 ]; then
|
||||
CORE_ARGS=(uvicorn app.main:app --host 0.0.0.0 --port 8000)
|
||||
[ $RELOAD -eq 1 ] && CORE_ARGS+=(--reload)
|
||||
start_in_dir "asf-core" "$ROOT_DIR/services/asf-core" "${CORE_ARGS[@]}"
|
||||
echo " → http://0.0.0.0:8000/docs (OpenAPI)"
|
||||
fi
|
||||
|
||||
# Web (Next.js dev)
|
||||
if [ $WEB -eq 1 ]; then
|
||||
start_in_dir "asf-web" "$ROOT_DIR/frontend/asf-web" npm run dev -- --hostname 0.0.0.0 --port 3000
|
||||
echo " → http://0.0.0.0:3000 (UI)"
|
||||
fi
|
||||
|
||||
# Worker
|
||||
if [ $WITH_WORKER -eq 1 ]; then
|
||||
start_in_dir "asf-worker" "$ROOT_DIR/services/asf-worker" python -m app.worker
|
||||
echo " → queues=$ASF_WORKER_JOB_QUEUES"
|
||||
fi
|
||||
|
||||
echo
|
||||
# Save PIDs on disk for `stop-stack.sh`
|
||||
: > "$PID_DIR/asf-stack.pids"
|
||||
for i in "${!pids[@]}"; do
|
||||
echo "${names[$i]} ${pids[$i]}" >> "$PID_DIR/asf-stack.pids"
|
||||
done
|
||||
echo "[start-stack] PIDs salvati in $PID_DIR/asf-stack.pids"
|
||||
echo
|
||||
echo "Tipi 'Ctrl+C' per arrestare TUTTI i processi. Oppure usa: ./scripts/stop-stack.sh"
|
||||
echo
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 5) Smoke test (first 20s)
|
||||
# ------------------------------------------------------------
|
||||
deadline=$((SECONDS + 25))
|
||||
ok_core=0; ok_web=0
|
||||
while [ $SECONDS -lt $deadline ]; do
|
||||
if [ $CORE -eq 1 ] && [ $ok_core -eq 0 ]; then
|
||||
code=$(curl -s -m 4 -o /dev/null -w '%{http_code}' http://127.0.0.1:8000/api/v1/health || true)
|
||||
[ "$code" = "200" ] && { ok_core=1; echo " ✅ asf-core up /health HTTP=$code"; }
|
||||
fi
|
||||
if [ $WEB -eq 1 ] && [ $ok_web -eq 0 ]; then
|
||||
code=$(curl -s -m 4 -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/ || true)
|
||||
[ "$code" = "200" ] && { ok_web=1; echo " ✅ asf-web up / HTTP=$code"; }
|
||||
fi
|
||||
need=0
|
||||
[ $CORE -eq 1 ] && [ $ok_core -eq 0 ] && need=1
|
||||
[ $WEB -eq 1 ] && [ $ok_web -eq 0 ] && need=1
|
||||
[ $need -eq 0 ] && break
|
||||
sleep 2
|
||||
done
|
||||
[ $CORE -eq 1 ] && [ $ok_core -eq 0 ] && echo " ⚠️ asf-core non ha risposto /health in tempo (guarda $LOG_DIR/asf-core.log)"
|
||||
[ $WEB -eq 1 ] && [ $ok_web -eq 0 ] && echo " ⚠️ asf-web non ha risposto / in tempo (Next.js 14 first compile slow: attendi / guarda $LOG_DIR/asf-web.log)"
|
||||
|
||||
echo
|
||||
echo "ASF stack ready. In attesa (foreground)… Ctrl+C = stop."
|
||||
|
||||
# wait
|
||||
for pid in "${pids[@]}"; do
|
||||
wait "$pid" || true
|
||||
done
|
||||
graceful_stop
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
# status-stack.sh — PIDs, logs, HTTP live per ASF Core/Web/Worker.
|
||||
set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PID_FILE="$ROOT_DIR/var/run/asf-stack.pids"
|
||||
LOG_DIR="$ROOT_DIR/var/log/asf"
|
||||
|
||||
echo "ASF stack STATUS $(date '+%F %T %Z')"
|
||||
echo
|
||||
printf "%-18s %-8s %-6s %-6s %s\n" "NAME" "PID" "PORT" "HTTP" "LOG"
|
||||
printf '─%.0s' $(seq 1 90); echo
|
||||
|
||||
check_http() {
|
||||
local url="$1"
|
||||
curl -s -m 3 -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || echo "000"
|
||||
}
|
||||
|
||||
# Core
|
||||
pid=$(pgrep -f "uvicorn app.main:app" 2>/dev/null | head -1 || true)
|
||||
http=$(check_http "http://127.0.0.1:8000/api/v1/health")
|
||||
printf "%-18s %-8s %-6s %-6s %s\n" "asf-core" "${pid:--}" "8000" "$http" "$LOG_DIR/asf-core.log"
|
||||
|
||||
# Web
|
||||
pid=$(pgrep -af "next-server|next dev" 2>/dev/null | awk '{print $1}' | head -1 || true)
|
||||
http=$(check_http "http://127.0.0.1:3000/")
|
||||
printf "%-18s %-8s %-6s %-6s %s\n" "asf-web" "${pid:--}" "3000" "$http" "$LOG_DIR/asf-web.log"
|
||||
|
||||
# Worker
|
||||
pid=$(pgrep -f "python -m app.worker" 2>/dev/null | head -1 || true)
|
||||
printf "%-18s %-8s %-6s %-6s %s\n" "asf-worker" "${pid:--}" "AMQP" "-" "$LOG_DIR/asf-worker.log"
|
||||
|
||||
echo
|
||||
echo "Pid file:"; [ -f "$PID_FILE" ] && cat "$PID_FILE" || echo " (nessuno)"
|
||||
echo
|
||||
echo "Logs tail (10 lines most recent each):"
|
||||
for f in "$LOG_DIR"/*.log; do
|
||||
[ -e "$f" ] || continue
|
||||
echo
|
||||
echo "───── $f (last 10 lines) ─────"
|
||||
tail -10 "$f" | sed 's/^/ │ /'
|
||||
done
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# stop-stack.sh — ferma i processi avviati da start-stack.sh.
|
||||
# Legge i PIDs da var/run/asf-stack.pids, oppure cerca per pattern.
|
||||
|
||||
set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PID_FILE="$ROOT_DIR/var/run/asf-stack.pids"
|
||||
|
||||
echo "ASF stack STOP $(date '+%F %T %Z')"
|
||||
echo
|
||||
|
||||
stop_group() {
|
||||
local pid="$1" name="$2"
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
echo " ✗ $name (pid=$pid) già spento"
|
||||
return 0
|
||||
fi
|
||||
echo -n " ⏹ $name pid=$pid → "
|
||||
kill -TERM "$pid" 2>/dev/null || true
|
||||
local w=0
|
||||
while [ $w -lt 8 ]; do
|
||||
kill -0 "$pid" 2>/dev/null || break
|
||||
sleep 1; w=$((w+1))
|
||||
done
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo -n "SIGKILL "
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
kill -0 "$pid" 2>/dev/null && echo "FAIL (ancora vivo)" || echo "ok"
|
||||
}
|
||||
|
||||
stop_patterns() {
|
||||
echo "[fallback] kill via pattern match…"
|
||||
for pattern in \
|
||||
"uvicorn app.main:app" \
|
||||
"next dev" "next-server" \
|
||||
"python -m app.worker" "asf-worker.*run_worker"; do
|
||||
pids=$(pgrep -af "$pattern" 2>/dev/null | awk '{print $1}' || true)
|
||||
[ -z "$pids" ] && continue
|
||||
echo " pattern '$pattern' → pids=$pids"
|
||||
for p in $pids; do stop_group "$p" "$pattern"; done
|
||||
done
|
||||
}
|
||||
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
echo "[from pidfile] $PID_FILE"
|
||||
while IFS=' ' read -r name pid rest; do
|
||||
[ -z "${name:-}" ] && continue
|
||||
[ -z "${pid:-}" ] && continue
|
||||
stop_group "$pid" "$name"
|
||||
done < "$PID_FILE"
|
||||
rm -f "$PID_FILE"
|
||||
else
|
||||
echo "[no pidfile $PID_FILE]"
|
||||
stop_patterns
|
||||
fi
|
||||
|
||||
# final sweep
|
||||
echo
|
||||
echo "ports still listening:"
|
||||
ss -ltnp 2>/dev/null | grep -E ":(8000|3000) " || echo " → nessuna porta 8000 / 3000 usata (ok)."
|
||||
echo "done."
|
||||
Reference in New Issue
Block a user