123 lines
4.3 KiB
YAML
123 lines
4.3 KiB
YAML
name: Audit production runtime (read-only)
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: audit-production-runtime
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
audit:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Inspect production over SSH
|
|
uses: appleboy/ssh-action@v1.0.3
|
|
with:
|
|
host: ${{ secrets.EC2_HOST }}
|
|
username: ubuntu
|
|
key: ${{ secrets.EC2_SSH_KEY }}
|
|
command_timeout: 10m
|
|
script: |
|
|
set -eu
|
|
cd ~/app
|
|
|
|
printf '%s\n' '=== source ==='
|
|
printf 'git_sha='
|
|
git rev-parse HEAD
|
|
printf 'git_branch='
|
|
git branch --show-current
|
|
|
|
cd infra/docker
|
|
ai_id=$(sudo docker compose -f docker-compose.prod.yml ps -q ai-service)
|
|
web_id=$(sudo docker compose -f docker-compose.prod.yml ps -q web)
|
|
postgres_id=$(sudo docker compose -f docker-compose.prod.yml ps -q postgres)
|
|
qdrant_id=$(sudo docker compose -f docker-compose.prod.yml ps -q qdrant)
|
|
test -n "$ai_id"
|
|
test -n "$web_id"
|
|
test -n "$postgres_id"
|
|
test -n "$qdrant_id"
|
|
|
|
printf '%s\n' '=== containers ==='
|
|
for entry in "ai-service:$ai_id" "web:$web_id" "postgres:$postgres_id" "qdrant:$qdrant_id"; do
|
|
service=${entry%%:*}
|
|
container=${entry#*:}
|
|
state=$(sudo docker inspect --format '{{.State.Status}}' "$container")
|
|
image_id=$(sudo docker inspect --format '{{.Image}}' "$container")
|
|
printf '%s state=%s image_id=%s\n' "$service" "$state" "$image_id"
|
|
done
|
|
|
|
printf '%s\n' '=== ai_runtime_contract ==='
|
|
sudo docker exec -i "$ai_id" python - <<'PY'
|
|
import json
|
|
|
|
from config import Settings
|
|
|
|
settings = Settings()
|
|
safe_fields = (
|
|
"app_name",
|
|
"environment",
|
|
"qdrant_url",
|
|
"qdrant_collection",
|
|
"embedding_provider",
|
|
"embedding_dimensions",
|
|
"evidence_minimum_score",
|
|
"aws_region",
|
|
"answer_provider",
|
|
"answer_model_id",
|
|
"rerank_enabled",
|
|
"metrics_enabled",
|
|
"otel_enabled",
|
|
"otel_service_name",
|
|
"otel_exporter_otlp_endpoint",
|
|
"otel_sample_ratio",
|
|
"entities_path",
|
|
"max_wall_clock_ms",
|
|
"max_llm_calls_per_turn",
|
|
)
|
|
contract = {name: str(getattr(settings, name)) for name in safe_fields}
|
|
print(json.dumps(contract, ensure_ascii=True, sort_keys=True))
|
|
PY
|
|
|
|
printf '%s\n' '=== persistent_mounts ==='
|
|
for entry in "postgres:$postgres_id" "qdrant:$qdrant_id"; do
|
|
service=${entry%%:*}
|
|
container=${entry#*:}
|
|
sudo docker inspect --format \
|
|
"$service {{range .Mounts}}{{.Type}}:{{.Name}}:{{.Destination}} {{end}}" \
|
|
"$container"
|
|
done
|
|
|
|
printf '%s\n' '=== datastore_identity ==='
|
|
sudo docker exec -i "$ai_id" python - <<'PY'
|
|
import json
|
|
import urllib.request
|
|
|
|
from config import Settings
|
|
|
|
settings = Settings()
|
|
url = settings.qdrant_url.rstrip("/") + "/collections/" + settings.qdrant_collection
|
|
with urllib.request.urlopen(url, timeout=10) as response:
|
|
payload = json.load(response)
|
|
result = payload.get("result", {})
|
|
config = result.get("config", {}).get("params", {}).get("vectors", {})
|
|
print(json.dumps({
|
|
"collection": settings.qdrant_collection,
|
|
"points_count": result.get("points_count"),
|
|
"status": result.get("status"),
|
|
"vector_config": config,
|
|
}, ensure_ascii=True, sort_keys=True))
|
|
PY
|
|
|
|
printf '%s\n' '=== health ==='
|
|
sudo docker exec -i "$ai_id" python - <<'PY'
|
|
import urllib.request
|
|
|
|
for path in ("/health", "/ready"):
|
|
with urllib.request.urlopen("http://127.0.0.1:8000" + path, timeout=10) as response:
|
|
print(path, response.status)
|
|
PY
|