Skip to content

chore(release): sync v9.5.0 from source #741

chore(release): sync v9.5.0 from source

chore(release): sync v9.5.0 from source #741

Workflow file for this run

name: SDK Smoke Tests
# Validates that SDK examples and README curl commands work against the stack.
# Catches broken documentation before merge.
#
# Runtime: ~4-5 minutes
# Trigger: Changes to agent, orchestrator, demo examples, docker-compose, or enterprise code
on:
pull_request:
branches:
- main
- develop
merge_group:
branches:
- main
- develop
push:
branches:
- main
- develop
paths:
- 'platform/agent/**'
- 'platform/orchestrator/**'
- 'platform/connectors/**'
- 'ee/platform/agent/**'
- 'ee/platform/orchestrator/**'
- 'ee/platform/connectors/**'
- 'examples/demo/**'
- 'docker-compose.yml'
- 'docker-compose.enterprise.yml'
- 'migrations/**'
- 'scripts/e2e/**'
- 'scripts/setup-e2e-testing.sh'
- 'scripts/operators/provision-app-role.sh'
- '.github/workflows/sdk-smoke-tests.yml'
workflow_dispatch:
permissions:
contents: read
pull-requests: read
# Cancel orphaned PR runs when a new commit is pushed. Pushes to main / merge_group
# runs use the SHA so they never share a group and never cancel each other.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
AXONFLOW_TELEMETRY: 'off'
# Timeouts and retry configuration
HEALTH_CHECK_RETRIES: 30
HEALTH_CHECK_INTERVAL: 2
DOCKER_WAIT_TIMEOUT: 180
jobs:
# =============================================================================
# Change Detection (PR and merge_group only)
# =============================================================================
detect-changes:
name: Detect Changes
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
permissions:
contents: read
pull-requests: read
outputs:
sdk-relevant: ${{ steps.filter.outputs.sdk-relevant }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Detect SDK-relevant changes
uses: dorny/paths-filter@v4
id: filter
with:
filters: |
sdk-relevant:
- 'platform/agent/**'
- 'platform/orchestrator/**'
- 'platform/connectors/**'
- 'ee/platform/agent/**'
- 'ee/platform/orchestrator/**'
- 'ee/platform/connectors/**'
- 'examples/demo/**'
- 'docker-compose.yml'
- 'docker-compose.enterprise.yml'
- 'migrations/**'
- 'scripts/e2e/**'
- 'scripts/setup-e2e-testing.sh'
- 'scripts/operators/provision-app-role.sh'
- '.github/workflows/sdk-smoke-tests.yml'
smoke-tests:
name: SDK Smoke Tests
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [detect-changes]
# Run if: push/workflow_dispatch OR (PR/merge_group with SDK-relevant changes)
if: |
always() &&
(github.event_name == 'push' ||
github.event_name == 'workflow_dispatch' ||
(needs.detect-changes.result == 'success' && needs.detect-changes.outputs.sdk-relevant == 'true'))
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install requests
# jq is pre-installed on GitHub runners, but verify
jq --version
- name: Start AxonFlow stack
id: start-stack
run: |
echo "🚀 Starting AxonFlow stack..."
docker compose up -d --wait --wait-timeout ${{ env.DOCKER_WAIT_TIMEOUT }} || {
echo "❌ Docker compose failed to start"
echo "📋 Docker compose logs:"
docker compose logs
exit 1
}
echo "✅ Docker compose started"
- name: Wait for services to be healthy
shell: bash
run: |
echo "⏳ Waiting for services to be healthy..."
# Function to check health with retries
wait_for_health() {
local url=$1
local name=$2
local retries=${{ env.HEALTH_CHECK_RETRIES }}
local interval=${{ env.HEALTH_CHECK_INTERVAL }}
for i in $(seq 1 $retries); do
if curl -sf "$url" > /dev/null 2>&1; then
echo "✅ $name is healthy"
return 0
fi
echo "⏳ Waiting for $name... (attempt $i/$retries)"
sleep $interval
done
echo "❌ $name failed to become healthy after $retries attempts"
return 1
}
# Check both services
wait_for_health "http://localhost:8080/health" "Agent" || exit 1
wait_for_health "http://localhost:8081/health" "Orchestrator" || exit 1
echo ""
echo "✅ All services are healthy!"
- name: Test PII Detection (SSN - Redact Mode)
shell: bash
run: |
echo "🧪 Testing PII detection (SSN - redact mode)..."
response=$(curl -s -X POST http://localhost:8080/api/policy/pre-check \
-H "Content-Type: application/json" \
-d '{"user_token": "test-user", "client_id": "smoke-test", "query": "Look up customer with SSN 123-45-6789"}')
echo "Response: $response"
# SSN detection defaults to redact mode (approved=true, requires_redaction=true)
# Issue #891: PII now defaults to redact instead of block
if echo "$response" | jq -e '.requires_redaction == true' > /dev/null 2>&1; then
policy=$(echo "$response" | jq -r '.policies[0]')
echo "✅ SSN detected, flagged for redaction (policy: $policy)"
else
echo "❌ SSN not flagged for redaction"
echo "Expected: requires_redaction=true"
exit 1
fi
- name: Test PII Detection (Credit Card - Redact Mode)
shell: bash
run: |
echo "🧪 Testing PII detection (Credit Card - redact mode)..."
response=$(curl -s -X POST http://localhost:8080/api/policy/pre-check \
-H "Content-Type: application/json" \
-d '{"user_token": "test-user", "client_id": "smoke-test", "query": "Process payment for card 4111-1111-1111-1111"}')
echo "Response: $response"
# Credit card detection defaults to redact mode (approved=true, requires_redaction=true)
# Issue #891: PII now defaults to redact instead of block
if echo "$response" | jq -e '.requires_redaction == true' > /dev/null 2>&1; then
echo "✅ Credit card detected, flagged for redaction"
else
echo "❌ Credit card not flagged for redaction"
echo "Expected: requires_redaction=true"
exit 1
fi
- name: Test SQL Injection Blocking
shell: bash
run: |
echo "🧪 Testing SQL injection blocking..."
response=$(curl -s -X POST http://localhost:8080/api/policy/pre-check \
-H "Content-Type: application/json" \
-d '{"user_token": "test-user", "client_id": "smoke-test", "query": "SELECT * FROM users UNION SELECT * FROM passwords"}')
echo "Response: $response"
if echo "$response" | jq -e '.approved == false or .policyApproved == false or .blocked == true' > /dev/null 2>&1; then
echo "✅ SQL injection correctly blocked"
else
echo "❌ SQL injection not blocked"
echo "Expected: approved=false or blocked=true"
exit 1
fi
- name: Test Safe Query (Baseline)
shell: bash
run: |
echo "🧪 Testing safe query passes..."
response=$(curl -s -X POST http://localhost:8080/api/policy/pre-check \
-H "Content-Type: application/json" \
-d '{"user_token": "test-user", "client_id": "smoke-test", "query": "What is the weather today?"}')
echo "Response: $response"
# Safe query should be approved
if echo "$response" | jq -e '.approved == true or .policyApproved == true or .blocked == false' > /dev/null 2>&1; then
echo "✅ Safe query correctly approved"
else
echo "❌ Safe query incorrectly blocked"
echo "Expected: approved=true or blocked=false"
exit 1
fi
- name: Collect logs on failure
if: failure()
run: |
echo "📋 Collecting diagnostic logs..."
echo ""
echo "=== Docker Compose Status ==="
docker compose ps
echo ""
echo "=== Agent Logs (last 100 lines) ==="
docker compose logs --tail=100 agent 2>/dev/null || echo "(no agent logs)"
echo ""
echo "=== Orchestrator Logs (last 100 lines) ==="
docker compose logs --tail=100 orchestrator 2>/dev/null || echo "(no orchestrator logs)"
echo ""
echo "=== PostgreSQL Logs (last 50 lines) ==="
docker compose logs --tail=50 postgres 2>/dev/null || echo "(no postgres logs)"
- name: Cleanup
if: always()
run: |
docker compose down -v --remove-orphans 2>/dev/null || true
# =============================================================================
# e2e-production-posture (issue #2391 / PR-E1)
# =============================================================================
# Mandatory production-posture E2E. Boots the enterprise stack under
# AXONFLOW_DB_USE_APP_ROLE=true with provisioned app-role + platform-admin
# DSNs, asserts every main pool actually connected AS axonflow_app_role,
# then runs the production-posture example suite from
# scripts/e2e/production_posture_registry.txt. Entries marked
# E2E-EXPECT-FAIL-UNTIL-PR-C1 are expected to fail with an RLS policy
# violation until PR-C1 lands the WithOrgScope wraps on the agent-side
# INSERT surface; the runner gates each entry against its expected outcome.
#
# Gating: This job is MANDATORY (the smoke-test-summary job below has it in
# `needs` and asserts result == 'success' or 'skipped'). v8.0.0 tag-cut is
# blocked on this job being green.
e2e-production-posture:
name: E2E Production-Posture (axonflow_app_role)
runs-on: ubuntu-latest
timeout-minutes: 30
# The production-posture job builds the enterprise compose stack via
# `docker compose -f docker-compose.yml -f docker-compose.enterprise.yml`,
# which requires docker-compose.enterprise.yml to be present in the
# working tree. That file is enterprise-only and is excluded from the
# community sync filter, so on community-mirror checkouts this job has
# no buildable target and must skip. `hashFiles()` is not allowed in a
# job-level `if:` expression (per GH Actions docs — `hashFiles` is only
# available inside `jobs.<job_id>.steps.*` contexts), so the gate uses
# `github.repository` instead: run only on the enterprise repo where
# the file ships; skip on the community mirror.
if: github.repository == 'getaxonflow/axonflow-enterprise'
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install postgresql-client (psql)
run: |
# provision-app-role.sh needs psql on the host (not in the postgres
# container) — it connects to postgres via the docker-published port
# 5432 from the GH runner shell.
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends postgresql-client
- name: Start AxonFlow stack with USE_APP_ROLE=false (so migrations run as master)
run: |
echo "Phase 1: boot stack under master role so mig 098 can CREATE ROLE"
# Pre-build images (the enterprise binary build is the long pole
# ~6-9 min cold; folding it into `up --wait` would race the
# wait-timeout). Splitting build from up gives the wait phase a
# clean budget. The dedicated portal-e2e workflow uses the same
# pattern for the same reason.
echo "Phase 1a: build images (no wait timeout — build phase is bounded by the job timeout)"
docker compose -f docker-compose.yml -f docker-compose.enterprise.yml build axonflow-agent axonflow-orchestrator axonflow-customer-portal || {
echo "::error::Docker compose build failed"
exit 1
}
echo "Phase 1b: up -d (no --wait — we'll poll /health manually with a long budget)"
docker compose -f docker-compose.yml -f docker-compose.enterprise.yml up -d || {
echo "::error::Docker compose up failed"
docker compose -f docker-compose.yml -f docker-compose.enterprise.yml logs
exit 1
}
echo "Phase 1c: wait for agent /health (migrations applied) — up to 240s"
healthy=0
for i in $(seq 1 120); do
if curl -sf http://localhost:8080/health >/dev/null 2>&1; then
echo "agent /health OK on attempt $i"
healthy=1
break
fi
sleep 2
done
if [ "$healthy" -ne 1 ]; then
echo "::error::agent never became healthy after 240s"
docker compose -f docker-compose.yml -f docker-compose.enterprise.yml ps
docker logs --tail=300 axonflow-agent 2>&1 || true
exit 1
fi
- name: Phase 2 — Provision axonflow_app_role + axonflow_platform_admin
env:
APP_ROLE_PASSWORD: ci-pp-app-role-pw
PLATFORM_ADMIN_PASSWORD: ci-pp-admin-pw
DATABASE_URL: postgres://axonflow:localdev123@127.0.0.1:5432/axonflow?sslmode=disable
run: |
set -euo pipefail
echo "Phase 2: ALTER ROLE both roles via scripts/operators/provision-app-role.sh"
bash scripts/operators/provision-app-role.sh
- name: Phase 3 — Restart agent/orchestrator/customer-portal with USE_APP_ROLE=true
run: |
set -euo pipefail
echo "Phase 3: append app-role env vars to .env and recreate the 3 main services"
touch .env
# Strip any prior entries to keep .env clean across re-runs.
sed -i '/^AXONFLOW_DB_USE_APP_ROLE=/d' .env
sed -i '/^AXONFLOW_DB_APP_ROLE_URL=/d' .env
sed -i '/^AXONFLOW_DB_PLATFORM_ADMIN_URL=/d' .env
echo "AXONFLOW_DB_USE_APP_ROLE=true" >> .env
echo "AXONFLOW_DB_APP_ROLE_URL=postgres://axonflow_app_role:ci-pp-app-role-pw@postgres:5432/axonflow?sslmode=disable" >> .env
echo "AXONFLOW_DB_PLATFORM_ADMIN_URL=postgres://axonflow_platform_admin:ci-pp-admin-pw@postgres:5432/axonflow?sslmode=disable" >> .env
docker compose -f docker-compose.yml -f docker-compose.enterprise.yml up -d \
--no-deps --force-recreate \
axonflow-agent axonflow-orchestrator axonflow-customer-portal
echo "Waiting for /health on all 3 services post-restart..."
# Each service gets up to 60×2s = 120s. If any fails to come up,
# exit 1 — falling through silently would make the Phase-4 probe
# produce a misleading "service emitted zero log output" error
# instead of the real "container failed to restart" error.
for svc_port in "agent:8080" "orchestrator:8081" "customer-portal:8082"; do
port="${svc_port##*:}"
name="${svc_port%%:*}"
healthy=0
for i in $(seq 1 60); do
if curl -sf "http://localhost:${port}/health" >/dev/null 2>&1; then
echo "$name /health OK on attempt $i"
healthy=1
break
fi
sleep 2
done
if [ "$healthy" -ne 1 ]; then
echo "::error::$name never became healthy after 60 attempts (~120s) post-restart."
echo "=== docker compose ps ==="
docker compose -f docker-compose.yml -f docker-compose.enterprise.yml ps || true
echo "=== last 200 lines of axonflow-$name logs ==="
docker logs --tail=200 "axonflow-$name" 2>&1 || true
exit 1
fi
done
- name: Phase 4 — Boot-log probe (MANDATORY)
run: |
set -euo pipefail
echo "Phase 4: assert every main pool is on axonflow_app_role"
bash scripts/e2e/probe-boot-log.sh
- name: Phase 5 — Run production-posture suite
env:
# SUITE_EXPECT_BOOT_RLS_EVIDENCE=0 (clean-boot regression-guard mode).
# Issue #2400 closed by wrapping ee/.../heartbeat.go::sendHeartbeat
# (the EE mirror copied over the community variant by the
# enterprise Dockerfile). With that wrap in place the production-
# posture boot is RLS-violation-free. Any new un-wrapped INSERT
# into an RLS-gated table surfaces here.
SUITE_EXPECT_BOOT_RLS_EVIDENCE: '0'
run: |
set -euo pipefail
echo "Phase 5: run scripts/e2e/run-production-posture-suite.sh"
echo " SUITE_EXPECT_BOOT_RLS_EVIDENCE=$SUITE_EXPECT_BOOT_RLS_EVIDENCE (clean-boot regression guard)"
bash scripts/e2e/run-production-posture-suite.sh
- name: Dump diagnostic logs on failure
if: failure()
run: |
set +e
echo "=== docker compose ps ==="
docker compose -f docker-compose.yml -f docker-compose.enterprise.yml ps
echo ""
for svc in axonflow-agent axonflow-orchestrator axonflow-customer-portal axonflow-postgres; do
echo "=== logs: $svc (last 300 lines) ==="
docker logs --tail=300 "$svc" 2>&1 || true
echo ""
echo "=== current_user= boot-log lines from $svc ==="
docker logs "$svc" 2>&1 | grep -E 'current_user=' || true
echo ""
done
- name: Cleanup
if: always()
run: |
docker compose -f docker-compose.yml -f docker-compose.enterprise.yml down -v --remove-orphans 2>/dev/null || true
smoke-test-summary:
name: Smoke Test Summary
runs-on: ubuntu-latest
needs: [detect-changes, smoke-tests, e2e-production-posture]
if: always()
steps:
- name: Check results
run: |
echo "🧪 SDK Smoke Tests Complete"
echo ""
echo "Results:"
echo " Detect Changes: ${{ needs.detect-changes.result }}"
echo " Smoke Tests: ${{ needs.smoke-tests.result }}"
echo " E2E Production-Posture: ${{ needs.e2e-production-posture.result }}"
- name: Fail if tests failed
# smoke-tests: success or skipped (skipped = no SDK-relevant changes).
# e2e-production-posture: success on enterprise checkouts where the
# enterprise compose file is present + drives the build. On
# community-mirror checkouts the job's job-level `if:` (hashFiles
# on docker-compose.enterprise.yml) skips it because the enterprise
# compose file is excluded from the sync filter and there's no
# buildable target. Accept `skipped` here for that case; `failure`
# still fails the summary.
if: |
needs.detect-changes.result == 'failure' ||
(needs.smoke-tests.result != 'success' && needs.smoke-tests.result != 'skipped') ||
(needs.e2e-production-posture.result != 'success' && needs.e2e-production-posture.result != 'skipped')
run: |
echo "❌ SDK smoke tests failed"
echo " smoke-tests: ${{ needs.smoke-tests.result }}"
echo " e2e-production-posture: ${{ needs.e2e-production-posture.result }}"
exit 1
- name: Success message
if: success()
run: |
echo "✅ SDK smoke tests passed!"
echo ""
echo "Validated:"
echo " - PII Detection (SSN - redact mode, requires_redaction=true)"
echo " - PII Detection (Credit Card - redact mode, requires_redaction=true)"
echo " - SQL Injection Blocking (approved=false)"
echo " - Safe Query Approval (approved=true)"
echo " - Production-Posture E2E: every main pool on axonflow_app_role (#2391)"