first commit
This commit is contained in:
0
.github/workflows/configs/default/.gitkeep
vendored
Normal file
0
.github/workflows/configs/default/.gitkeep
vendored
Normal file
57
.github/workflows/configs/default/config.json
vendored
Normal file
57
.github/workflows/configs/default/config.json
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"$schema": "https://www.getbifrost.ai/schema",
|
||||
"config_store": {
|
||||
"enabled": true,
|
||||
"type": "postgres",
|
||||
"config": {
|
||||
"host": "localhost",
|
||||
"port": "5432",
|
||||
"user": "bifrost",
|
||||
"password": "bifrost_password",
|
||||
"db_name": "bifrost",
|
||||
"ssl_mode": "disable"
|
||||
}
|
||||
},
|
||||
"logs_store": {
|
||||
"enabled": true,
|
||||
"type": "postgres",
|
||||
"config": {
|
||||
"host": "localhost",
|
||||
"port": "5432",
|
||||
"user": "bifrost",
|
||||
"password": "bifrost_password",
|
||||
"db_name": "bifrost",
|
||||
"ssl_mode": "disable"
|
||||
}
|
||||
},
|
||||
"providers": {
|
||||
"openai": {
|
||||
"keys": [
|
||||
{
|
||||
"name": "e2e-openai-key",
|
||||
"value": "env.OPENAI_API_KEY",
|
||||
"weight": 1,
|
||||
"models": ["*"],
|
||||
"use_for_batch_api": true
|
||||
}
|
||||
],
|
||||
"network_config": {
|
||||
"default_request_timeout_in_seconds": 300
|
||||
}
|
||||
},
|
||||
"anthropic": {
|
||||
"keys": [
|
||||
{
|
||||
"name": "e2e-anthropic-key",
|
||||
"value": "env.ANTHROPIC_API_KEY",
|
||||
"weight": 1,
|
||||
"models": ["*"],
|
||||
"use_for_batch_api": true
|
||||
}
|
||||
],
|
||||
"network_config": {
|
||||
"default_request_timeout_in_seconds": 300
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
107
.github/workflows/configs/docker-compose.yml
vendored
Normal file
107
.github/workflows/configs/docker-compose.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: bifrost
|
||||
POSTGRES_PASSWORD: bifrost_password
|
||||
POSTGRES_DB: bifrost
|
||||
PGDATA: /var/lib/postgresql/data/pgdata
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U bifrost -d bifrost"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- bifrost_network
|
||||
|
||||
weaviate:
|
||||
image: cr.weaviate.io/semitechnologies/weaviate:1.32.4
|
||||
command:
|
||||
- --host
|
||||
- 0.0.0.0
|
||||
- --port
|
||||
- '8080'
|
||||
- --scheme
|
||||
- http
|
||||
environment:
|
||||
- CLUSTER_HOSTNAME=weaviate
|
||||
- CLUSTER_ADVERTISE_ADDR=172.38.0.12
|
||||
- CLUSTER_GOSSIP_BIND_PORT=7946
|
||||
- CLUSTER_DATA_BIND_PORT=7947
|
||||
- DISABLE_TELEMETRY=true
|
||||
- PERSISTENCE_DATA_PATH=/var/lib/weaviate
|
||||
- DEFAULT_VECTORIZER_MODULE=none
|
||||
- ENABLE_MODULES=
|
||||
- AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true
|
||||
- LOG_LEVEL=info
|
||||
ports:
|
||||
- "9000:8080"
|
||||
volumes:
|
||||
- weaviate_data:/var/lib/weaviate
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/v1/.well-known/ready"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
bifrost_network:
|
||||
ipv4_address: 172.38.0.12
|
||||
|
||||
# Redis Stack instance for vector store tests
|
||||
redis-stack:
|
||||
image: redis/redis-stack:7.4.0-v6
|
||||
command: redis-stack-server --protected-mode no
|
||||
ports:
|
||||
- "6379:6379"
|
||||
- "8001:8001" # RedisInsight web UI
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
bifrost_network:
|
||||
ipv4_address: 172.38.0.13
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Qdrant instance for vector store tests
|
||||
qdrant:
|
||||
image: qdrant/qdrant:v1.16.0
|
||||
ports:
|
||||
- "6333:6333" # REST API
|
||||
- "6334:6334" # gRPC API
|
||||
volumes:
|
||||
- qdrant_data:/qdrant/storage
|
||||
networks:
|
||||
bifrost_network:
|
||||
ipv4_address: 172.38.0.14
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/6333'"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
networks:
|
||||
bifrost_network:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.38.0.0/16
|
||||
gateway: 172.38.0.1
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
weaviate_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
qdrant_data:
|
||||
driver: local
|
||||
|
||||
0
.github/workflows/configs/emptystate/.gitkeep
vendored
Normal file
0
.github/workflows/configs/emptystate/.gitkeep
vendored
Normal file
9
.github/workflows/configs/noconfigstorenologstore/config.json
vendored
Normal file
9
.github/workflows/configs/noconfigstorenologstore/config.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "https://www.getbifrost.ai/schema",
|
||||
"config_store": {
|
||||
"enabled": false
|
||||
},
|
||||
"logs_store": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
27
.github/workflows/configs/witconfigstorelogstorepostgres/config.json
vendored
Normal file
27
.github/workflows/configs/witconfigstorelogstorepostgres/config.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"$schema": "https://www.getbifrost.ai/schema",
|
||||
"config_store": {
|
||||
"enabled": true,
|
||||
"type": "postgres",
|
||||
"config": {
|
||||
"host": "localhost",
|
||||
"port": "5432",
|
||||
"user": "bifrost",
|
||||
"password": "bifrost_password",
|
||||
"db_name": "bifrost",
|
||||
"ssl_mode": "disable"
|
||||
}
|
||||
},
|
||||
"logs_store": {
|
||||
"enabled": true,
|
||||
"type": "postgres",
|
||||
"config": {
|
||||
"host": "localhost",
|
||||
"port": "5432",
|
||||
"user": "bifrost",
|
||||
"password": "bifrost_password",
|
||||
"db_name": "bifrost",
|
||||
"ssl_mode": "disable"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
.github/workflows/configs/withconfigstore/config.json
vendored
Normal file
10
.github/workflows/configs/withconfigstore/config.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://www.getbifrost.ai/schema",
|
||||
"config_store": {
|
||||
"enabled": true,
|
||||
"type": "sqlite",
|
||||
"config": {
|
||||
"path": "../.github/workflows/configs/withconfigstore/config.db"
|
||||
}
|
||||
}
|
||||
}
|
||||
27
.github/workflows/configs/withconfigstorelogsstorepostgres/config.json
vendored
Normal file
27
.github/workflows/configs/withconfigstorelogsstorepostgres/config.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"$schema": "https://www.getbifrost.ai/schema",
|
||||
"config_store": {
|
||||
"enabled": true,
|
||||
"type": "postgres",
|
||||
"config": {
|
||||
"host": "localhost",
|
||||
"port": "5432",
|
||||
"user": "bifrost",
|
||||
"password": "bifrost_password",
|
||||
"db_name": "bifrost",
|
||||
"ssl_mode": "disable"
|
||||
}
|
||||
},
|
||||
"logs_store": {
|
||||
"enabled": true,
|
||||
"type": "postgres",
|
||||
"config": {
|
||||
"host": "localhost",
|
||||
"port": "5432",
|
||||
"user": "bifrost",
|
||||
"password": "bifrost_password",
|
||||
"db_name": "bifrost",
|
||||
"ssl_mode": "disable"
|
||||
}
|
||||
}
|
||||
}
|
||||
17
.github/workflows/configs/withconfigstorelogsstoresqlite/config.json
vendored
Normal file
17
.github/workflows/configs/withconfigstorelogsstoresqlite/config.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://www.getbifrost.ai/schema",
|
||||
"config_store": {
|
||||
"enabled": true,
|
||||
"type": "sqlite",
|
||||
"config": {
|
||||
"path": "../.github/workflows/configs/withconfigstorelogsstoresqlite/config.db"
|
||||
}
|
||||
},
|
||||
"logs_store": {
|
||||
"enabled": true,
|
||||
"type": "sqlite",
|
||||
"config": {
|
||||
"path": "../.github/workflows/configs/withconfigstorelogsstoresqlite/logs.db"
|
||||
}
|
||||
}
|
||||
}
|
||||
17
.github/workflows/configs/withdynamicplugin/config.json
vendored
Normal file
17
.github/workflows/configs/withdynamicplugin/config.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://www.getbifrost.ai/schema",
|
||||
"config_store": {
|
||||
"enabled": true,
|
||||
"type": "sqlite",
|
||||
"config": {
|
||||
"path": "../.github/workflows/configs/withdynamicplugin/config.db"
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "hello-world",
|
||||
"path": "../examples/plugins/hello-world/build/hello-world.so"
|
||||
}
|
||||
]
|
||||
}
|
||||
29
.github/workflows/configs/withobservability/config.json
vendored
Normal file
29
.github/workflows/configs/withobservability/config.json
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://www.getbifrost.ai/schema",
|
||||
"config_store": {
|
||||
"enabled": true,
|
||||
"type": "sqlite",
|
||||
"config": {
|
||||
"path": "../.github/workflows/configs/withobservability/config.db"
|
||||
}
|
||||
},
|
||||
"logs_store": {
|
||||
"enabled": true,
|
||||
"type": "sqlite",
|
||||
"config": {
|
||||
"path": "../.github/workflows/configs/withobservability/logs.db"
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "otel",
|
||||
"config": {
|
||||
"service_name": "bifrost",
|
||||
"collector_url": "http://localhost:4318/v1/traces",
|
||||
"trace_type": "genai_extension",
|
||||
"protocol": "http"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
150
.github/workflows/configs/withpostgresmcpclientsinconfig/config.json
vendored
Normal file
150
.github/workflows/configs/withpostgresmcpclientsinconfig/config.json
vendored
Normal file
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"$schema": "https://www.getbifrost.ai/schema",
|
||||
"client": {
|
||||
"allow_direct_keys": false,
|
||||
"allowed_origins": [
|
||||
"*"
|
||||
],
|
||||
"disable_content_logging": false,
|
||||
"drop_excess_requests": false,
|
||||
"enable_logging": true,
|
||||
"enforce_auth_on_inference": true,
|
||||
"initial_pool_size": 300,
|
||||
"log_retention_days": 365,
|
||||
"max_request_body_size_mb": 100
|
||||
},
|
||||
"config_store": {
|
||||
"enabled": true,
|
||||
"type": "postgres",
|
||||
"config": {
|
||||
"host": "localhost",
|
||||
"port": "5432",
|
||||
"user": "bifrost",
|
||||
"password": "bifrost_password",
|
||||
"db_name": "bifrost",
|
||||
"ssl_mode": "disable"
|
||||
}
|
||||
},
|
||||
"logs_store": {
|
||||
"enabled": true,
|
||||
"type": "postgres",
|
||||
"config": {
|
||||
"host": "localhost",
|
||||
"port": "5432",
|
||||
"user": "bifrost",
|
||||
"password": "bifrost_password",
|
||||
"db_name": "bifrost",
|
||||
"ssl_mode": "disable"
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"client_configs": [
|
||||
{
|
||||
"name": "WeatherService",
|
||||
"connection_type": "http",
|
||||
"client_id": "weather-mcp-server",
|
||||
"connection_string": "http://localhost:8080/mcp"
|
||||
},
|
||||
{
|
||||
"name": "CalendarService",
|
||||
"connection_type": "http",
|
||||
"client_id": "calendar-mcp-server",
|
||||
"connection_string": "http://localhost:8081/mcp"
|
||||
}
|
||||
]
|
||||
},
|
||||
"governance": {
|
||||
"auth_config": {
|
||||
"admin_password": "env.BIFROST_ADMIN_PASSWORD",
|
||||
"admin_username": "env.BIFROST_ADMIN_USERNAME",
|
||||
"disable_auth_on_inference": true,
|
||||
"is_enabled": false
|
||||
},
|
||||
"virtual_keys": [
|
||||
{
|
||||
"id": "vk-ai-portal-prod",
|
||||
"is_active": true,
|
||||
"name": "ai-portal-production-key",
|
||||
"description": "Virtual key for AI portal with MCP access to weather and calendar services",
|
||||
"value": "env.BIFROST_VK_AI_PORTAL",
|
||||
"mcp_configs": [
|
||||
{
|
||||
"mcp_client_name": "WeatherService",
|
||||
"tools_to_execute": [
|
||||
"*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mcp_client_name": "CalendarService",
|
||||
"tools_to_execute": [
|
||||
"get_events",
|
||||
"create_event"
|
||||
]
|
||||
}
|
||||
],
|
||||
"provider_configs": [
|
||||
{
|
||||
"provider": "openai",
|
||||
"allowed_models": [
|
||||
"*"
|
||||
],
|
||||
"key_ids": [
|
||||
"*"
|
||||
],
|
||||
"weight": 1.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "vk-internal-tools",
|
||||
"is_active": true,
|
||||
"name": "internal-tools-key",
|
||||
"description": "Virtual key for internal tools with limited MCP access",
|
||||
"value": "env.BIFROST_VK_INTERNAL",
|
||||
"mcp_configs": [
|
||||
{
|
||||
"mcp_client_name": "WeatherService",
|
||||
"tools_to_execute": [
|
||||
"get_current_weather"
|
||||
]
|
||||
}
|
||||
],
|
||||
"provider_configs": [
|
||||
{
|
||||
"provider": "openai",
|
||||
"allowed_models": [
|
||||
"*"
|
||||
],
|
||||
"key_ids": [
|
||||
"*"
|
||||
],
|
||||
"weight": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"config": {
|
||||
"is_vk_mandatory": true
|
||||
},
|
||||
"enabled": true,
|
||||
"name": "governance"
|
||||
}
|
||||
],
|
||||
"providers": {
|
||||
"openai": {
|
||||
"keys": [
|
||||
{
|
||||
"name": "openai-primary",
|
||||
"value": "env.OPENAI_API_KEY",
|
||||
"weight": 1,
|
||||
"models": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
21
.github/workflows/configs/withsemanticcache/config.json
vendored
Normal file
21
.github/workflows/configs/withsemanticcache/config.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://www.getbifrost.ai/schema",
|
||||
"vector_store": {
|
||||
"enabled": true,
|
||||
"type": "weaviate",
|
||||
"config": {
|
||||
"scheme": "http",
|
||||
"host": "localhost:9000"
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "semantic_cache",
|
||||
"config": {
|
||||
"dimension": 1,
|
||||
"vector_store_namespace": "test"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
61
.github/workflows/dependabot-alerts.yml
vendored
Normal file
61
.github/workflows/dependabot-alerts.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: Dependabot Alerts to Issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 9 * * 1" # Weekly on Monday at 9am UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
create-issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.github.com:443
|
||||
|
||||
- name: Create issues from Dependabot alerts
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
alerts=$(gh api repos/${{ github.repository }}/dependabot/alerts \
|
||||
--jq '[.[] | select(.state == "open")]')
|
||||
|
||||
echo "$alerts" | jq -c '.[]' | while read -r alert; do
|
||||
pkg=$(echo "$alert" | jq -r '.dependency.package.name')
|
||||
number=$(echo "$alert" | jq -r '.number')
|
||||
severity=$(echo "$alert" | jq -r '.security_advisory.severity')
|
||||
summary=$(echo "$alert" | jq -r '.security_advisory.summary')
|
||||
url=$(echo "$alert" | jq -r '.html_url')
|
||||
ecosystem=$(echo "$alert" | jq -r '.dependency.package.ecosystem')
|
||||
|
||||
# Skip if issue already exists for this alert
|
||||
existing=$(gh issue list \
|
||||
--repo "${{ github.repository }}" \
|
||||
--search "Dependabot Alert #${number}" \
|
||||
--json number --jq 'length')
|
||||
|
||||
if [ "$existing" = "0" ]; then
|
||||
gh issue create \
|
||||
--repo "${{ github.repository }}" \
|
||||
--title "dep: update ${pkg} (${severity})" \
|
||||
--label "dependencies" \
|
||||
--body "$(cat <<EOF
|
||||
## Dependabot Alert #${number}
|
||||
|
||||
**Package:** \`${pkg}\`
|
||||
**Ecosystem:** ${ecosystem}
|
||||
**Severity:** ${severity}
|
||||
|
||||
${summary}
|
||||
|
||||
[View Alert](${url})
|
||||
EOF
|
||||
)"
|
||||
fi
|
||||
done
|
||||
32
.github/workflows/dependency-review.yml
vendored
Normal file
32
.github/workflows/dependency-review.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# Dependency Review Action
|
||||
#
|
||||
# This Action will scan dependency manifest files that change as part of a Pull Request,
|
||||
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
|
||||
# Once installed, if the workflow run is marked as required,
|
||||
# PRs introducing known-vulnerable packages will be blocked from merging.
|
||||
#
|
||||
# Source repository: https://github.com/actions/dependency-review-action
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.deps.dev:443
|
||||
api.github.com:443
|
||||
api.securityscorecards.dev:443
|
||||
github.com:443
|
||||
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
|
||||
46
.github/workflows/docs-validation.yml
vendored
Normal file
46
.github/workflows/docs-validation.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Docs Validation
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "docs/**"
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "docs/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-broken-links:
|
||||
name: Check Broken Links
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.github.com:443
|
||||
github.com:443
|
||||
nodejs.org:443
|
||||
ph.mintlify.com:443
|
||||
registry.npmjs.org:443
|
||||
release-assets.githubusercontent.com:443
|
||||
storage.googleapis.com:443
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "21"
|
||||
|
||||
- name: Check for broken links
|
||||
working-directory: ./docs
|
||||
run: |
|
||||
echo "Checking for broken links in documentation..."
|
||||
npx --yes mintlify@latest broken-links
|
||||
echo "✅ No broken links found"
|
||||
73
.github/workflows/e2e-tests.yml
vendored
Normal file
73
.github/workflows/e2e-tests.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["02-27-feat_extend_e2e_ui_tests"]
|
||||
|
||||
concurrency:
|
||||
group: e2e-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-e2e-ui:
|
||||
name: E2E UI (Playwright)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version: "1.26.2"
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "25"
|
||||
|
||||
- name: Set up Docker Compose
|
||||
run: |
|
||||
docker --version
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
echo "Installing Docker Compose plugin..."
|
||||
DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
|
||||
mkdir -p "$DOCKER_CONFIG/cli-plugins"
|
||||
curl -SL "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o "$DOCKER_CONFIG/cli-plugins/docker-compose"
|
||||
chmod +x "$DOCKER_CONFIG/cli-plugins/docker-compose"
|
||||
docker compose version
|
||||
else
|
||||
echo "Docker Compose plugin is available"
|
||||
docker compose version
|
||||
fi
|
||||
|
||||
- name: Run E2E UI tests
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# Optional: for SSE MCP tests (e.g. remote proxy). Set in repo secrets.
|
||||
MCP_SSE_HEADERS: ${{ secrets.MCP_SSE_HEADERS }}
|
||||
run: ./.github/workflows/scripts/test-e2e-ui.sh
|
||||
|
||||
- name: Upload Playwright artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: playwright-report
|
||||
path: |
|
||||
tests/e2e/test-results/
|
||||
tests/e2e/playwright-report/
|
||||
retention-days: 7
|
||||
129
.github/workflows/helm-release.yml
vendored
Normal file
129
.github/workflows/helm-release.yml
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
name: Release Helm Chart
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "helm-charts/bifrost/**"
|
||||
- ".github/workflows/helm-release.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.github.com:443
|
||||
get.helm.sh:443
|
||||
github.com:443
|
||||
maximhq.github.io:443
|
||||
proxy.golang.org:443
|
||||
release-assets.githubusercontent.com:443
|
||||
storage.googleapis.com:443
|
||||
sum.golang.org:443
|
||||
uploads.github.com:443
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "$GITHUB_ACTOR"
|
||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
|
||||
with:
|
||||
version: v4.0.0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version: "1.26.2"
|
||||
|
||||
- name: Run chart-testing (lint)
|
||||
run: |
|
||||
helm lint helm-charts/bifrost
|
||||
|
||||
- name: Validate Helm templates
|
||||
run: |
|
||||
chmod +x .github/workflows/scripts/validate-helm-templates.sh
|
||||
.github/workflows/scripts/validate-helm-templates.sh
|
||||
|
||||
- name: Validate Helm config fields
|
||||
run: |
|
||||
chmod +x .github/workflows/scripts/validate-helm-config-fields.sh
|
||||
.github/workflows/scripts/validate-helm-config-fields.sh
|
||||
|
||||
- name: Validate Go ↔ config.schema.json ↔ helm-chart sync (schemasync)
|
||||
run: |
|
||||
chmod +x .github/workflows/scripts/validate-schema-sync.sh
|
||||
.github/workflows/scripts/validate-schema-sync.sh
|
||||
|
||||
- name: Get chart version
|
||||
id: chart-version
|
||||
run: |
|
||||
VERSION=$(grep '^version:' helm-charts/bifrost/Chart.yaml | awk '{print $2}')
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Chart version: $VERSION"
|
||||
|
||||
- name: Check if release exists
|
||||
id: check-release
|
||||
run: |
|
||||
if gh release view "helm-chart-v${{ steps.chart-version.outputs.version }}" &>/dev/null; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Package Helm chart
|
||||
run: |
|
||||
cd helm-charts
|
||||
helm package bifrost
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: steps.check-release.outputs.exists == 'false'
|
||||
run: |
|
||||
cd helm-charts
|
||||
gh release create "helm-chart-v${{ steps.chart-version.outputs.version }}" \
|
||||
bifrost-${{ steps.chart-version.outputs.version }}.tgz \
|
||||
--title "Helm Chart v${{ steps.chart-version.outputs.version }}" \
|
||||
--notes "Helm chart release for Bifrost v${{ steps.chart-version.outputs.version }}"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Update Helm repository index
|
||||
run: |
|
||||
cd helm-charts
|
||||
# Download existing index if it exists
|
||||
curl -sLO https://maximhq.github.io/bifrost/helm-charts/index.yaml || true
|
||||
# Merge with new chart only if index.yaml exists
|
||||
if [ -f index.yaml ]; then
|
||||
helm repo index . --url https://github.com/maximhq/bifrost/releases/download/helm-chart-v${{ steps.chart-version.outputs.version }} --merge index.yaml
|
||||
else
|
||||
helm repo index . --url https://github.com/maximhq/bifrost/releases/download/helm-chart-v${{ steps.chart-version.outputs.version }}
|
||||
fi
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/v1.5.0'
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./helm-charts
|
||||
destination_dir: helm-charts
|
||||
keep_files: false
|
||||
enable_jekyll: false
|
||||
user_name: "github-actions[bot]"
|
||||
user_email: "github-actions[bot]@users.noreply.github.com"
|
||||
302
.github/workflows/npx-publish.yml
vendored
Normal file
302
.github/workflows/npx-publish.yml
vendored
Normal file
@@ -0,0 +1,302 @@
|
||||
name: NPX Package Publish
|
||||
|
||||
# Triggers when main is pushed and package.json has changed
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'npx/bifrost/package.json'
|
||||
- 'npx/bifrost-cli/package.json'
|
||||
|
||||
# Prevent concurrent runs for the same trigger
|
||||
concurrency:
|
||||
group: npx-publish-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# Check if pipeline should be skipped based on first line of commit message
|
||||
check-skip:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
should-skip: ${{ steps.check.outputs.should-skip }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Check if pipeline should be skipped
|
||||
id: check
|
||||
run: |
|
||||
COMMIT_MESSAGE=$(git log -1 --pretty=%B)
|
||||
FIRST_LINE=$(echo "$COMMIT_MESSAGE" | head -n 1)
|
||||
if [[ "$FIRST_LINE" == *"--skip-ci"* ]]; then
|
||||
echo "should-skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "should-skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
publish-bifrost:
|
||||
needs: [check-skip]
|
||||
if: needs.check-skip.outputs.should-skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write # Required for npm provenance
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "25"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
npx/bifrost/package-lock.json
|
||||
|
||||
- name: Check if bifrost package.json changed
|
||||
id: check-bifrost
|
||||
run: |
|
||||
if git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" | grep -q '^npx/bifrost/package.json$'; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Extract version from package.json
|
||||
if: steps.check-bifrost.outputs.changed == 'true'
|
||||
id: extract-version
|
||||
run: ./.github/workflows/scripts/extract-npx-version.sh
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.check-bifrost.outputs.changed == 'true'
|
||||
working-directory: npx/bifrost
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests
|
||||
if: steps.check-bifrost.outputs.changed == 'true'
|
||||
working-directory: npx/bifrost
|
||||
run: |
|
||||
if [ -f "package.json" ] && npm run | grep -q "test"; then
|
||||
echo "Running tests..."
|
||||
npm test
|
||||
else
|
||||
echo "No tests found, skipping..."
|
||||
fi
|
||||
|
||||
- name: Publish to npm
|
||||
if: steps.check-bifrost.outputs.changed == 'true'
|
||||
working-directory: npx/bifrost
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
VERSION="${{ steps.extract-version.outputs.version }}"
|
||||
echo "Publishing @maximhq/bifrost@${VERSION} to npm..."
|
||||
if npm view @maximhq/bifrost@"${VERSION}" version >/dev/null 2>&1; then
|
||||
echo "@maximhq/bifrost@${VERSION} already exists on npm. Skipping publish."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Try OIDC (Trusted Publishing) first - no token needed
|
||||
echo "Attempting publish with OIDC (Trusted Publishing)..."
|
||||
if npm publish --provenance --access public 2>&1; then
|
||||
echo "Published successfully with OIDC!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Fallback to NPM_TOKEN if OIDC fails
|
||||
if [ -n "$NPM_TOKEN" ]; then
|
||||
echo "OIDC failed, falling back to NPM_TOKEN..."
|
||||
export NODE_AUTH_TOKEN="$NPM_TOKEN"
|
||||
npm publish --access public
|
||||
else
|
||||
echo "OIDC failed and no NPM_TOKEN available"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Configure Git
|
||||
if: steps.check-bifrost.outputs.changed == 'true'
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: steps.check-bifrost.outputs.changed == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: bash .github/workflows/scripts/create-npx-release.sh "${{ steps.extract-version.outputs.version }}" "${{ steps.extract-version.outputs.full-tag }}"
|
||||
|
||||
- name: Discord Notification
|
||||
if: always() && steps.check-bifrost.outputs.changed == 'true'
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
run: |
|
||||
AUTHOR="${{ github.actor }}"
|
||||
COMMIT_AUTHOR="$(git log -1 --pretty=%an || true)"
|
||||
if [ -n "$COMMIT_AUTHOR" ]; then AUTHOR="$COMMIT_AUTHOR"; fi
|
||||
if [ "${{ job.status }}" = "success" ]; then
|
||||
TITLE="**NPX Package Published**"
|
||||
STATUS="Success"
|
||||
VERSION_LINE="**Version**: \`${{ steps.extract-version.outputs.version }}\`"
|
||||
PACKAGE_LINE="**Package**: \`@maximhq/bifrost\`"
|
||||
NPM_LINK="**[View on npm](https://www.npmjs.com/package/@maximhq/bifrost)**"
|
||||
MESSAGE="$TITLE\n**Status**: $STATUS\n$VERSION_LINE\n$PACKAGE_LINE\n$NPM_LINK\n**Tag**: \`${{ steps.extract-version.outputs.full-tag }}\`\n**Commit**: \`${{ github.sha }}\`\n**Author**: ${AUTHOR}\n**[View Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})**"
|
||||
else
|
||||
TITLE="**NPX Package Publish Failed**"
|
||||
STATUS="Failed"
|
||||
MESSAGE="$TITLE\n**Status**: $STATUS\n**Tag**: \`${{ steps.extract-version.outputs.full-tag }}\`\n**Commit**: \`${{ github.sha }}\`\n**Author**: ${AUTHOR}\n**[View Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})**"
|
||||
fi
|
||||
payload="$(jq -n --arg content "$MESSAGE" '{content:$content}')"
|
||||
curl -sS -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK"
|
||||
|
||||
publish-bifrost-cli:
|
||||
needs: [check-skip]
|
||||
if: needs.check-skip.outputs.should-skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write # Required for npm provenance
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "25"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Check if bifrost-cli package.json changed
|
||||
id: check-cli
|
||||
run: |
|
||||
if git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" | grep -q '^npx/bifrost-cli/package.json$'; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Extract version
|
||||
if: steps.check-cli.outputs.changed == 'true'
|
||||
id: extract-version
|
||||
run: |
|
||||
VERSION=$(jq -r '.version' npx/bifrost-cli/package.json)
|
||||
if [[ -z "$VERSION" ]] || [[ "$VERSION" == "null" ]]; then
|
||||
echo "Failed to extract version from npx/bifrost-cli/package.json"
|
||||
exit 1
|
||||
fi
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "full-tag=npx/bifrost-cli/v${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "Extracted bifrost-cli version: ${VERSION}"
|
||||
|
||||
- name: Publish to npm
|
||||
if: steps.check-cli.outputs.changed == 'true'
|
||||
working-directory: npx/bifrost-cli
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
VERSION="${{ steps.extract-version.outputs.version }}"
|
||||
echo "Publishing @maximhq/bifrost-cli@${VERSION} to npm..."
|
||||
if npm view @maximhq/bifrost-cli@"${VERSION}" version >/dev/null 2>&1; then
|
||||
echo "@maximhq/bifrost-cli@${VERSION} already exists on npm. Skipping publish."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Try OIDC (Trusted Publishing) first - no token needed
|
||||
echo "Attempting publish with OIDC (Trusted Publishing)..."
|
||||
if npm publish --provenance --access public 2>&1; then
|
||||
echo "Published successfully with OIDC!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Fallback to NPM_TOKEN if OIDC fails
|
||||
if [ -n "$NPM_TOKEN" ]; then
|
||||
echo "OIDC failed, falling back to NPM_TOKEN..."
|
||||
export NODE_AUTH_TOKEN="$NPM_TOKEN"
|
||||
npm publish --access public
|
||||
else
|
||||
echo "OIDC failed and no NPM_TOKEN available"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Configure Git
|
||||
if: steps.check-cli.outputs.changed == 'true'
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: steps.check-cli.outputs.changed == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
VERSION="${{ steps.extract-version.outputs.version }}"
|
||||
FULL_TAG="${{ steps.extract-version.outputs.full-tag }}"
|
||||
PRERELEASE_FLAG=""
|
||||
if [[ "$VERSION" == *-* ]]; then
|
||||
PRERELEASE_FLAG="--prerelease"
|
||||
fi
|
||||
if gh release view "$FULL_TAG" >/dev/null 2>&1; then
|
||||
echo "Release $FULL_TAG already exists. Skipping."
|
||||
exit 0
|
||||
fi
|
||||
if ! git rev-parse "$FULL_TAG" >/dev/null 2>&1; then
|
||||
git tag "$FULL_TAG"
|
||||
git push origin "$FULL_TAG"
|
||||
fi
|
||||
gh release create "$FULL_TAG" \
|
||||
--title "Bifrost CLI v$VERSION" \
|
||||
--notes "## Bifrost CLI v$VERSION
|
||||
|
||||
Install or run via npx:
|
||||
|
||||
\`\`\`bash
|
||||
npx -y @maximhq/bifrost-cli
|
||||
\`\`\`
|
||||
|
||||
- [View on npm](https://www.npmjs.com/package/@maximhq/bifrost-cli)
|
||||
- [Documentation](https://docs.getbifrost.ai/quickstart/cli/getting-started)" \
|
||||
--latest=false \
|
||||
${PRERELEASE_FLAG}
|
||||
|
||||
- name: Discord Notification
|
||||
if: always() && steps.check-cli.outputs.changed == 'true'
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
run: |
|
||||
AUTHOR="${{ github.actor }}"
|
||||
COMMIT_AUTHOR="$(git log -1 --pretty=%an || true)"
|
||||
if [ -n "$COMMIT_AUTHOR" ]; then AUTHOR="$COMMIT_AUTHOR"; fi
|
||||
if [ "${{ job.status }}" = "success" ]; then
|
||||
MESSAGE="**NPX Bifrost CLI Published**\n**Status**: Success\n**Version**: \`${{ steps.extract-version.outputs.version }}\`\n**Package**: \`@maximhq/bifrost-cli\`\n**[View on npm](https://www.npmjs.com/package/@maximhq/bifrost-cli)**\n**Commit**: \`${{ github.sha }}\`\n**Author**: ${AUTHOR}\n**[View Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})**"
|
||||
else
|
||||
MESSAGE="**NPX Bifrost CLI Publish Failed**\n**Status**: Failed\n**Commit**: \`${{ github.sha }}\`\n**Author**: ${AUTHOR}\n**[View Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})**"
|
||||
fi
|
||||
payload="$(jq -n --arg content "$MESSAGE" '{content:$content}')"
|
||||
curl -sS -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK"
|
||||
65
.github/workflows/openapi-bundle.yml
vendored
Normal file
65
.github/workflows/openapi-bundle.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
name: OpenAPI Bundle
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- ".github/workflows/openapi-bundle.yml"
|
||||
- "docs/openapi/**"
|
||||
- "!docs/openapi/openapi.json"
|
||||
pull_request:
|
||||
paths:
|
||||
- "docs/openapi/**"
|
||||
- "!docs/openapi/openapi.json"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bundle-openapi:
|
||||
name: Bundle OpenAPI Spec
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
files.pythonhosted.org:443
|
||||
github.com:443
|
||||
pypi.org:443
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "GitHub Actions Bot"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install pyyaml
|
||||
|
||||
- name: Bundle OpenAPI spec
|
||||
working-directory: ./docs/openapi
|
||||
run: python bundle.py
|
||||
|
||||
- name: Commit and push changes
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
CURRENT_BRANCH="${GITHUB_REF_NAME:-main}"
|
||||
|
||||
git add docs/openapi/openapi.json
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "chore: regenerate openapi.json --skip-ci"
|
||||
git push origin "$CURRENT_BRANCH"
|
||||
59
.github/workflows/pr-test-notifier.yml
vendored
Normal file
59
.github/workflows/pr-test-notifier.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
name: PR Test Notifier
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
# Check if pipeline should be skipped based on first line of commit message
|
||||
check-skip:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should-skip: ${{ steps.check.outputs.should-skip }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Check if pipeline should be skipped
|
||||
id: check
|
||||
run: |
|
||||
COMMIT_MESSAGE=$(git log -1 --pretty=%B)
|
||||
FIRST_LINE=$(echo "$COMMIT_MESSAGE" | head -n 1)
|
||||
if [[ "$FIRST_LINE" == *"--skip-ci"* ]]; then
|
||||
echo "should-skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "should-skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
notify:
|
||||
needs: [check-skip]
|
||||
if: needs.check-skip.outputs.should-skip != 'true'
|
||||
name: Post Test Instructions
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Post comment with test trigger instructions
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh pr comment ${{ github.event.pull_request.number }} \
|
||||
--repo ${{ github.repository }} \
|
||||
--body "## 🧪 Test Suite Available
|
||||
|
||||
This PR can be tested by a repository admin.
|
||||
|
||||
[Run tests for PR #${{ github.event.pull_request.number }}](https://github.com/${{ github.repository }}/actions/workflows/pr-tests.yml)"
|
||||
163
.github/workflows/pr-tests.yml
vendored
Normal file
163
.github/workflows/pr-tests.yml
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
name: PR Tests (Requires Approval)
|
||||
|
||||
on:
|
||||
# Manual trigger only - requires admin to click "Run workflow" button
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: "PR number to test (leave empty for current branch)"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
# Prevent concurrent test runs on the same PR
|
||||
concurrency:
|
||||
group: pr-tests-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# Check if pipeline should be skipped based on first line of commit message
|
||||
check-skip:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
should-skip: ${{ steps.check.outputs.should-skip }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Check if pipeline should be skipped
|
||||
id: check
|
||||
run: |
|
||||
COMMIT_MESSAGE=$(git log -1 --pretty=%B)
|
||||
FIRST_LINE=$(echo "$COMMIT_MESSAGE" | head -n 1)
|
||||
if [[ "$FIRST_LINE" == *"--skip-ci"* ]]; then
|
||||
echo "should-skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "should-skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# This job shows up immediately and waits for approval
|
||||
run-tests:
|
||||
needs: [check-skip]
|
||||
if: needs.check-skip.outputs.should-skip != 'true'
|
||||
name: Run Tests (Awaiting Approval)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Environment with protection rules - requires admin approval
|
||||
# Note: You need to configure this environment in repo settings
|
||||
environment:
|
||||
name: pr-testing
|
||||
url: ${{ github.event.pull_request.html_url || github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version: "1.26.2"
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "25"
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Add comment to PR
|
||||
if: github.event.pull_request.number
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh pr comment ${{ github.event.pull_request.number }} --body "🧪 Test run approved and starting...
|
||||
|
||||
**Test Suite Includes:**
|
||||
- 📦 Core Build Validation
|
||||
- 🔌 MCP Test Servers Build
|
||||
- 🔧 Core Provider Tests
|
||||
- 🛡️ Governance Tests
|
||||
- 🔗 Integration Tests
|
||||
|
||||
[View workflow run →](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
|
||||
|
||||
- name: Make test script executable
|
||||
run: chmod +x .github/workflows/scripts/run-tests.sh
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
# API Keys for provider tests
|
||||
MAXIM_API_KEY: ${{ secrets.MAXIM_API_KEY }}
|
||||
MAXIM_LOGGER_ID: ${{ secrets.MAXIM_LOG_REPO_ID }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_SESSION_TOKEN: ${{ secrets.AWS_SESSION_TOKEN }}
|
||||
AWS_ARN: ${{ secrets.AWS_ARN }}
|
||||
BEDROCK_API_KEY: ${{ secrets.BEDROCK_API_KEY }}
|
||||
AZURE_ENDPOINT: ${{ secrets.AZURE_ENDPOINT }}
|
||||
AZURE_API_KEY: ${{ secrets.AZURE_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
PARASAIL_API_KEY: ${{ secrets.PARASAIL_API_KEY }}
|
||||
PERPLEXITY_API_KEY: ${{ secrets.PERPLEXITY_API_KEY }}
|
||||
ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }}
|
||||
SGL_API_KEY: ${{ secrets.SGL_API_KEY }}
|
||||
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
|
||||
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||
VERTEX_CREDENTIALS: ${{ secrets.VERTEX_CREDENTIALS }}
|
||||
VERTEX_PROJECT_ID: ${{ secrets.VERTEX_PROJECT_ID }}
|
||||
HUGGING_FACE_API_KEY: ${{ secrets.HUGGING_FACE_API_KEY }}
|
||||
REPLICATE_API_KEY: ${{ secrets.REPLICATE_API_KEY }}
|
||||
REPLICATE_OWNER : ${{ secrets.REPLICATE_OWNER }}
|
||||
RUNWAY_API_KEY : ${{ secrets.RUNWAY_API_KEY }}
|
||||
run: |
|
||||
echo "Running tests for PR #${{ github.event.pull_request.number || 'manual run' }}"
|
||||
./.github/workflows/scripts/run-tests.sh
|
||||
|
||||
- name: Report test results
|
||||
if: always() && github.event.pull_request.number
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
if [ "${{ job.status }}" = "success" ]; then
|
||||
gh pr comment ${{ github.event.pull_request.number }} --body "✅ **All tests passed successfully!**
|
||||
|
||||
All test suites have completed without errors. This PR is ready for review.
|
||||
|
||||
[View detailed results →](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
|
||||
else
|
||||
gh pr comment ${{ github.event.pull_request.number }} --body "❌ **Tests failed**
|
||||
|
||||
One or more test suites failed. Please review the failures and update your PR.
|
||||
|
||||
[View detailed results →](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
|
||||
fi
|
||||
138
.github/workflows/release-cli.yml
vendored
Normal file
138
.github/workflows/release-cli.yml
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
name: Release CLI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
# Prevent concurrent runs
|
||||
concurrency:
|
||||
group: release-cli
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.get-version.outputs.version }}
|
||||
tag_exists: ${{ steps.check-tag.outputs.exists }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
github.com:443
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Get version from file
|
||||
id: get-version
|
||||
run: echo "version=$(cat cli/version)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check if tag exists
|
||||
id: check-tag
|
||||
run: |
|
||||
if git rev-parse "cli/v${{ steps.get-version.outputs.version }}" >/dev/null 2>&1; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
test-cli:
|
||||
needs: [check-version]
|
||||
if: needs.check-version.outputs.tag_exists == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version: "1.26.2"
|
||||
|
||||
- name: Run CLI tests
|
||||
working-directory: cli
|
||||
run: go test ./...
|
||||
|
||||
release-cli:
|
||||
needs: [check-version, test-cli]
|
||||
if: needs.check-version.outputs.tag_exists == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
outputs:
|
||||
success: ${{ steps.release.outputs.success }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version: "1.26.2"
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "GitHub Actions Bot"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Release CLI
|
||||
id: release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
run: ./.github/workflows/scripts/release-cli.sh "${{ needs.check-version.outputs.version }}"
|
||||
|
||||
push-mintlify-changelog:
|
||||
needs: [check-version, release-cli]
|
||||
if: needs.check-version.outputs.tag_exists == 'false' && needs.release-cli.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Push Mintlify changelog
|
||||
run: |
|
||||
./.github/workflows/scripts/push-cli-mintlify-changelog.sh "${{ needs.check-version.outputs.version }}"
|
||||
1822
.github/workflows/release-pipeline.yml
vendored
Normal file
1822
.github/workflows/release-pipeline.yml
vendored
Normal file
File diff suppressed because it is too large
Load Diff
94
.github/workflows/scorecards.yml
vendored
Normal file
94
.github/workflows/scorecards.yml
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
# This workflow uses actions that are not certified by GitHub. They are provided
|
||||
# by a third-party and are governed by separate terms of service, privacy
|
||||
# policy, and support documentation.
|
||||
|
||||
name: Scorecard supply-chain security
|
||||
on:
|
||||
# For Branch-Protection check. Only the default branch is supported. See
|
||||
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
|
||||
branch_protection_rule:
|
||||
# To guarantee Maintained check is occasionally updated. See
|
||||
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
|
||||
schedule:
|
||||
- cron: '20 7 * * 2'
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecard analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload the results to code-scanning dashboard.
|
||||
security-events: write
|
||||
# Needed to publish results and get a badge (see publish_results below).
|
||||
id-token: write
|
||||
contents: read
|
||||
actions: read
|
||||
# To allow GraphQL ListCommits to work
|
||||
issues: read
|
||||
pull-requests: read
|
||||
# To detect SAST tools
|
||||
checks: read
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.deps.dev:443
|
||||
api.github.com:443
|
||||
api.osv.dev:443
|
||||
api.scorecard.dev:443
|
||||
auth.docker.io:443
|
||||
fulcio.sigstore.dev:443
|
||||
github.com:443
|
||||
index.docker.io:443
|
||||
oss-fuzz-build-logs.storage.googleapis.com:443
|
||||
rekor.sigstore.dev:443
|
||||
tuf-repo-cdn.sigstore.dev:443
|
||||
www.bestpractices.dev:443
|
||||
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
|
||||
# - you want to enable the Branch-Protection check on a *public* repository, or
|
||||
# - you are installing Scorecards on a *private* repository
|
||||
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
|
||||
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
|
||||
|
||||
# Public repositories:
|
||||
# - Publish results to OpenSSF REST API for easy access by consumers
|
||||
# - Allows the repository to include the Scorecard badge.
|
||||
# - See https://github.com/ossf/scorecard-action#publishing-results.
|
||||
# For private repositories:
|
||||
# - `publish_results` will always be set to `false`, regardless
|
||||
# of the value entered here.
|
||||
publish_results: true
|
||||
|
||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@5c8a8a642e79153f5d047b10ec1cba1d1cc65699 # v3.35.1
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
59
.github/workflows/scripts/build-cli-executables.sh
vendored
Executable file
59
.github/workflows/scripts/build-cli-executables.sh
vendored
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Cross-compile CLI binaries for multiple platforms
|
||||
# Usage: ./build-cli-executables.sh <version>
|
||||
|
||||
if [[ -z "${1:-}" ]]; then
|
||||
echo "Usage: $0 <version>" >&2
|
||||
exit 1
|
||||
fi
|
||||
VERSION="$1"
|
||||
|
||||
echo "🔨 Building CLI executables with version: $VERSION"
|
||||
|
||||
# Get the script directory and project root
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
|
||||
# Clean and create dist directory
|
||||
rm -rf "$PROJECT_ROOT/dist"
|
||||
mkdir -p "$PROJECT_ROOT/dist"
|
||||
|
||||
# Define platforms
|
||||
platforms=(
|
||||
"darwin/amd64"
|
||||
"darwin/arm64"
|
||||
"linux/amd64"
|
||||
"linux/arm64"
|
||||
"windows/amd64"
|
||||
)
|
||||
|
||||
MODULE_PATH="$PROJECT_ROOT/cli"
|
||||
COMMIT="${GITHUB_SHA:-$(git rev-parse HEAD 2>/dev/null || echo 'unknown')}"
|
||||
|
||||
for platform in "${platforms[@]}"; do
|
||||
IFS='/' read -r GOOS GOARCH <<< "$platform"
|
||||
|
||||
output_name="bifrost"
|
||||
[[ "$GOOS" = "windows" ]] && output_name+='.exe'
|
||||
|
||||
echo "Building bifrost CLI for $GOOS/$GOARCH..."
|
||||
mkdir -p "$PROJECT_ROOT/dist/$GOOS/$GOARCH"
|
||||
|
||||
cd "$MODULE_PATH"
|
||||
|
||||
# CLI has no CGO dependencies, so we can cross-compile without cross-compilers
|
||||
env GOWORK=off CGO_ENABLED=0 GOOS="$GOOS" GOARCH="$GOARCH" \
|
||||
go build -trimpath \
|
||||
-ldflags "-s -w -buildid= -X main.version=v${VERSION} -X main.commit=${COMMIT}" \
|
||||
-o "$PROJECT_ROOT/dist/$GOOS/$GOARCH/$output_name" .
|
||||
|
||||
# Generate SHA-256 checksum for the binary
|
||||
(cd "$PROJECT_ROOT/dist/$GOOS/$GOARCH" && shasum -a 256 "$output_name" > "$output_name.sha256")
|
||||
echo " → checksum: $(cat "$PROJECT_ROOT/dist/$GOOS/$GOARCH/$output_name.sha256")"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
done
|
||||
|
||||
echo "✅ All CLI binaries built successfully"
|
||||
125
.github/workflows/scripts/build-executables.sh
vendored
Executable file
125
.github/workflows/scripts/build-executables.sh
vendored
Executable file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Cross-compile Go binaries for multiple platforms
|
||||
# Usage: ./build-executables.sh <version> [platforms]
|
||||
# Examples:
|
||||
# ./build-executables.sh 1.4.15 # Build all platforms
|
||||
# ./build-executables.sh 1.4.15 "darwin/amd64 darwin/arm64 linux/amd64 windows/amd64" # Build specific platforms
|
||||
# ./build-executables.sh 1.4.15 "linux/arm64" # Build single platform (native on ARM)
|
||||
|
||||
# Require version argument (matches usage)
|
||||
if [[ -z "${1:-}" ]]; then
|
||||
echo "Usage: $0 <version> [platforms]" >&2
|
||||
exit 1
|
||||
fi
|
||||
VERSION="$1"
|
||||
PLATFORM_FILTER="${2:-}"
|
||||
|
||||
echo "🔨 Building Go executables with version: $VERSION"
|
||||
|
||||
# Get the script directory and project root
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
|
||||
# Clean and create dist directory
|
||||
rm -rf "$PROJECT_ROOT/dist"
|
||||
mkdir -p "$PROJECT_ROOT/dist"
|
||||
|
||||
# Define platforms — use filter if provided, otherwise build all
|
||||
all_platforms=(
|
||||
"darwin/amd64"
|
||||
"darwin/arm64"
|
||||
"linux/amd64"
|
||||
"linux/arm64"
|
||||
"windows/amd64"
|
||||
)
|
||||
|
||||
if [[ -n "$PLATFORM_FILTER" ]]; then
|
||||
platforms=()
|
||||
for p in $PLATFORM_FILTER; do
|
||||
platforms+=("$p")
|
||||
done
|
||||
echo "📋 Building filtered platforms: ${platforms[*]}"
|
||||
else
|
||||
platforms=("${all_platforms[@]}")
|
||||
echo "📋 Building all platforms: ${platforms[*]}"
|
||||
fi
|
||||
|
||||
# Detect host architecture for native build detection
|
||||
HOST_ARCH=$(uname -m)
|
||||
|
||||
MODULE_PATH="$PROJECT_ROOT/transports/bifrost-http"
|
||||
|
||||
|
||||
for platform in "${platforms[@]}"; do
|
||||
IFS='/' read -r PLATFORM_DIR GOARCH <<< "$platform"
|
||||
|
||||
case "$PLATFORM_DIR" in
|
||||
"windows") GOOS="windows" ;;
|
||||
"darwin") GOOS="darwin" ;;
|
||||
"linux") GOOS="linux" ;;
|
||||
*) echo "Unsupported platform: $PLATFORM_DIR"; exit 1 ;;
|
||||
esac
|
||||
|
||||
output_name="bifrost-http"
|
||||
[[ "$GOOS" = "windows" ]] && output_name+='.exe'
|
||||
|
||||
echo "Building bifrost-http for $PLATFORM_DIR/$GOARCH..."
|
||||
mkdir -p "$PROJECT_ROOT/dist/$PLATFORM_DIR/$GOARCH"
|
||||
|
||||
# Change to the module directory for building
|
||||
cd "$MODULE_PATH"
|
||||
|
||||
if [[ "$GOOS" = "linux" ]]; then
|
||||
# Detect native build: if target arch matches host, use system compiler
|
||||
if [[ "$GOARCH" = "arm64" ]] && [[ "$HOST_ARCH" = "aarch64" || "$HOST_ARCH" = "arm64" ]]; then
|
||||
echo " 🏠 Native ARM64 build detected — using system compiler"
|
||||
CC_COMPILER="${CC:-gcc}"
|
||||
CXX_COMPILER="${CXX:-g++}"
|
||||
elif [[ "$GOARCH" = "amd64" ]] && [[ "$HOST_ARCH" = "x86_64" ]]; then
|
||||
echo " 🏠 Native AMD64 build detected — using system compiler"
|
||||
CC_COMPILER="${CC:-gcc}"
|
||||
CXX_COMPILER="${CXX:-g++}"
|
||||
elif [[ "$GOARCH" = "amd64" ]]; then
|
||||
CC_COMPILER="x86_64-linux-musl-gcc"
|
||||
CXX_COMPILER="x86_64-linux-musl-g++"
|
||||
elif [[ "$GOARCH" = "arm64" ]]; then
|
||||
CC_COMPILER="aarch64-linux-musl-gcc"
|
||||
CXX_COMPILER="aarch64-linux-musl-g++"
|
||||
fi
|
||||
|
||||
env GOWORK=off CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" CC="$CC_COMPILER" CXX="$CXX_COMPILER" \
|
||||
go build -trimpath -tags "netgo,osusergo,sqlite_static" \
|
||||
-ldflags "-s -w -buildid= -extldflags '-static' -X main.Version=v${VERSION}" \
|
||||
-o "$PROJECT_ROOT/dist/$PLATFORM_DIR/$GOARCH/$output_name" .
|
||||
|
||||
elif [[ "$GOOS" = "windows" ]]; then
|
||||
if [[ "$GOARCH" = "amd64" ]]; then
|
||||
CC_COMPILER="x86_64-w64-mingw32-gcc"
|
||||
CXX_COMPILER="x86_64-w64-mingw32-g++"
|
||||
fi
|
||||
|
||||
env GOWORK=off CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" CC="$CC_COMPILER" CXX="$CXX_COMPILER" \
|
||||
go build -trimpath -ldflags "-s -w -buildid= -X main.Version=v${VERSION}" \
|
||||
-o "$PROJECT_ROOT/dist/$PLATFORM_DIR/$GOARCH/$output_name" .
|
||||
|
||||
else # Darwin (macOS)
|
||||
if [[ "$GOARCH" = "amd64" ]]; then
|
||||
CC_COMPILER="o64-clang"
|
||||
CXX_COMPILER="o64-clang++"
|
||||
elif [[ "$GOARCH" = "arm64" ]]; then
|
||||
CC_COMPILER="oa64-clang"
|
||||
CXX_COMPILER="oa64-clang++"
|
||||
fi
|
||||
|
||||
env GOWORK=off CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" CC="$CC_COMPILER" CXX="$CXX_COMPILER" \
|
||||
go build -trimpath -ldflags "-s -w -buildid= -X main.Version=v${VERSION}" \
|
||||
-o "$PROJECT_ROOT/dist/$PLATFORM_DIR/$GOARCH/$output_name" .
|
||||
fi
|
||||
|
||||
# Change back to project root
|
||||
cd "$PROJECT_ROOT"
|
||||
done
|
||||
|
||||
echo "✅ All binaries built successfully"
|
||||
19
.github/workflows/scripts/changelog-utils.sh
vendored
Normal file
19
.github/workflows/scripts/changelog-utils.sh
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Function to extract content from a file
|
||||
# Usage: get_file_content <file_path>
|
||||
# Returns the file content with comments removed, or empty string if file doesn't exist
|
||||
get_file_content() {
|
||||
if [ -f "$1" ]; then
|
||||
content=$(cat "$1")
|
||||
# Skip comments from content
|
||||
content=$(echo "$content" | grep -v '^<!--' | grep -v '^-->')
|
||||
# For version files, also trim newlines and whitespace
|
||||
if [[ "$1" == *"/version" ]]; then
|
||||
content=$(echo "$content" | tr -d '\n' | xargs)
|
||||
fi
|
||||
echo "$content"
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
81
.github/workflows/scripts/check-dependency-flow.sh
vendored
Executable file
81
.github/workflows/scripts/check-dependency-flow.sh
vendored
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Check the dependency flow and suggest next steps
|
||||
# Usage: ./check-dependency-flow.sh <stage> [version]
|
||||
# stage: core|framework|plugins
|
||||
# version: required for core/framework; optional for plugins
|
||||
usage() {
|
||||
echo "Usage: $0 <stage: core|framework|plugins> [version]" >&2
|
||||
echo "Examples:" >&2
|
||||
echo " $0 core v1.2.3" >&2
|
||||
echo " $0 framework v1.2.3" >&2
|
||||
echo " $0 plugins" >&2
|
||||
}
|
||||
if [[ $# -lt 1 ]]; then
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
STAGE="${1:-}"
|
||||
VERSION="${2:-}"
|
||||
|
||||
# Validate stage first, then enforce version requirement by stage
|
||||
case "$STAGE" in
|
||||
core|framework|plugins)
|
||||
;;
|
||||
*)
|
||||
echo "❌ Unknown stage: $STAGE" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# VERSION is required for core/framework; optional for plugins
|
||||
if [[ "$STAGE" != "plugins" && -z "${VERSION:-}" ]]; then
|
||||
echo "❌ VERSION is required for stage '$STAGE'." >&2
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
|
||||
case "$STAGE" in
|
||||
"core")
|
||||
echo "🔧 Core v$VERSION released!"
|
||||
echo ""
|
||||
echo "📋 Dependency Flow Status:"
|
||||
echo "✅ Core: v$VERSION (just released)"
|
||||
echo "❓ Framework: Check if update needed"
|
||||
echo "❓ Plugins: Will check after framework"
|
||||
echo "❓ Bifrost HTTP: Will check after plugins"
|
||||
echo ""
|
||||
echo "🔄 Next Step: Manually trigger Framework Release if needed"
|
||||
;;
|
||||
|
||||
"framework")
|
||||
echo "📦 Framework v$VERSION released!"
|
||||
echo ""
|
||||
echo "📋 Dependency Flow Status:"
|
||||
echo "✅ Core: (already updated)"
|
||||
echo "✅ Framework: v$VERSION (just released)"
|
||||
echo "❓ Plugins: Check if any need updates"
|
||||
echo "❓ Bifrost HTTP: Will check after plugins"
|
||||
echo ""
|
||||
echo "🔄 Next Step: Check Plugins Release workflow"
|
||||
;;
|
||||
|
||||
"plugins")
|
||||
echo "🔌 Plugins ${VERSION:+v$VERSION }released!"
|
||||
echo ""
|
||||
echo "📋 Dependency Flow Status:"
|
||||
echo "✅ Core: (already updated)"
|
||||
echo "✅ Framework: (already updated)"
|
||||
echo "✅ Plugins: (just released)"
|
||||
echo "❓ Bifrost HTTP: Check if update needed"
|
||||
echo ""
|
||||
echo "🔄 Next Step: Manually trigger Bifrost HTTP Release if needed"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "❌ Unknown stage: $STAGE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
31
.github/workflows/scripts/configure-r2.sh
vendored
Executable file
31
.github/workflows/scripts/configure-r2.sh
vendored
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Configure AWS CLI for R2 uploads
|
||||
# Usage: ./configure-r2.sh
|
||||
|
||||
echo "⚙️ Configuring AWS CLI for R2..."
|
||||
|
||||
pip install awscli
|
||||
|
||||
# Clean and trim environment variables (removing any whitespace)
|
||||
R2_ENDPOINT="$(echo "$R2_ENDPOINT" | tr -d '[:space:]')"
|
||||
R2_ACCESS_KEY_ID="$(echo "$R2_ACCESS_KEY_ID" | tr -d '[:space:]')"
|
||||
R2_SECRET_ACCESS_KEY="$(echo "$R2_SECRET_ACCESS_KEY" | tr -d '[:space:]')"
|
||||
|
||||
# Validate environment variables
|
||||
if [ -z "$R2_ENDPOINT" ] || [ -z "$R2_ACCESS_KEY_ID" ] || [ -z "$R2_SECRET_ACCESS_KEY" ]; then
|
||||
echo "❌ Missing required R2 credentials"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Configure AWS CLI for R2 using dedicated profile
|
||||
aws configure set --profile R2 aws_access_key_id "$R2_ACCESS_KEY_ID"
|
||||
aws configure set --profile R2 aws_secret_access_key "$R2_SECRET_ACCESS_KEY"
|
||||
aws configure set --profile R2 region us-east-1
|
||||
aws configure set --profile R2 s3.signature_version s3v4
|
||||
|
||||
# Test connection
|
||||
echo "🔍 Testing R2 connection..."
|
||||
aws s3 ls s3://prod-downloads/ --endpoint-url "$R2_ENDPOINT" --profile R2 >/dev/null
|
||||
echo "✅ R2 connection successful"
|
||||
36
.github/workflows/scripts/create-docker-manifest.sh
vendored
Executable file
36
.github/workflows/scripts/create-docker-manifest.sh
vendored
Executable file
@@ -0,0 +1,36 @@
|
||||
# Validate input argument
|
||||
if [ "${1:-}" = "" ]; then
|
||||
echo "Usage: $0 <version>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="$1"
|
||||
REGISTRY="docker.io"
|
||||
ACCOUNT="maximhq"
|
||||
IMAGE_NAME="bifrost"
|
||||
IMAGE="${REGISTRY}/${ACCOUNT}/${IMAGE_NAME}"
|
||||
|
||||
# Get the actual image digests from the platform-specific builds
|
||||
AMD64_DIGEST=$(docker manifest inspect ${IMAGE}:v${VERSION}-amd64 | jq -r '.manifests[0].digest')
|
||||
ARM64_DIGEST=$(docker manifest inspect ${IMAGE}:v${VERSION}-arm64 | jq -r '.manifests[0].digest')
|
||||
|
||||
echo "AMD64 digest: ${AMD64_DIGEST}"
|
||||
echo "ARM64 digest: ${ARM64_DIGEST}"
|
||||
|
||||
# Create manifest for versioned tag using digests
|
||||
docker manifest create \
|
||||
${IMAGE}:v${VERSION} \
|
||||
${IMAGE}@${AMD64_DIGEST} \
|
||||
${IMAGE}@${ARM64_DIGEST}
|
||||
|
||||
docker manifest push ${IMAGE}:v${VERSION}
|
||||
|
||||
# Create latest manifest only for stable versions
|
||||
if [[ "$VERSION" != *-* ]]; then
|
||||
docker manifest create \
|
||||
${IMAGE}:latest \
|
||||
${IMAGE}@${AMD64_DIGEST} \
|
||||
${IMAGE}@${ARM64_DIGEST}
|
||||
|
||||
docker manifest push ${IMAGE}:latest
|
||||
fi
|
||||
92
.github/workflows/scripts/create-npx-release.sh
vendored
Executable file
92
.github/workflows/scripts/create-npx-release.sh
vendored
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Create GitHub release for NPX package
|
||||
# Usage: ./create-npx-release.sh <version> <full-tag>
|
||||
|
||||
VERSION="$1"
|
||||
FULL_TAG="$2"
|
||||
|
||||
if [[ -z "$VERSION" || -z "$FULL_TAG" ]]; then
|
||||
echo "❌ Usage: $0 <version> <full-tag>"
|
||||
exit 1
|
||||
fi
|
||||
# Mark prereleases when version contains a hyphen
|
||||
PRERELEASE_FLAG=""
|
||||
if [[ "$VERSION" == *-* ]]; then
|
||||
PRERELEASE_FLAG="--prerelease"
|
||||
fi
|
||||
TITLE="NPX Package v$VERSION"
|
||||
|
||||
# Create release body
|
||||
BODY="## NPX Package Release
|
||||
|
||||
### 📦 NPX Package v$VERSION
|
||||
|
||||
The Bifrost CLI is now available on npm!
|
||||
|
||||
### Installation
|
||||
|
||||
\`\`\`bash
|
||||
# Install globally
|
||||
npm install -g @maximhq/bifrost
|
||||
|
||||
# Or use with npx (no installation needed)
|
||||
npx @maximhq/bifrost --help
|
||||
\`\`\`
|
||||
|
||||
### Usage
|
||||
|
||||
\`\`\`bash
|
||||
# Start Bifrost HTTP server
|
||||
bifrost
|
||||
|
||||
# Use specific transport version
|
||||
bifrost --transport-version v1.2.3
|
||||
|
||||
# Get help
|
||||
bifrost --help
|
||||
\`\`\`
|
||||
|
||||
### Links
|
||||
|
||||
- 📦 [View on npm](https://www.npmjs.com/package/@maximhq/bifrost)
|
||||
- 📚 [Documentation](https://github.com/maximhq/bifrost)
|
||||
- 🐛 [Report Issues](https://github.com/maximhq/bifrost/issues)
|
||||
|
||||
### What's New
|
||||
|
||||
This NPX package provides a convenient way to run Bifrost without manual binary downloads. The CLI automatically:
|
||||
|
||||
- Detects your platform and architecture
|
||||
- Downloads the appropriate binary
|
||||
- Supports version pinning with \`--transport-version\`
|
||||
- Provides progress indicators for downloads
|
||||
|
||||
---
|
||||
_This release was automatically created from tag \`$FULL_TAG\`_"
|
||||
|
||||
# Check if release already exists
|
||||
echo "🔍 Checking if release $FULL_TAG already exists..."
|
||||
if gh release view "$FULL_TAG" >/dev/null 2>&1; then
|
||||
echo "ℹ️ Release $FULL_TAG already exists. Skipping creation."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if tag already exists
|
||||
echo "🔍 Checking if tag $FULL_TAG exists..."
|
||||
if git rev-parse "$FULL_TAG" >/dev/null 2>&1; then
|
||||
echo "✅ Tag $FULL_TAG already exists."
|
||||
else
|
||||
echo "🏷️ Creating tag $FULL_TAG..."
|
||||
git tag "$FULL_TAG"
|
||||
git push origin "$FULL_TAG"
|
||||
fi
|
||||
|
||||
# Create release
|
||||
echo "🎉 Creating GitHub release for $TITLE..."
|
||||
gh release create "$FULL_TAG" \
|
||||
--title "$TITLE" \
|
||||
--notes "$BODY" \
|
||||
--latest=false \
|
||||
${PRERELEASE_FLAG}
|
||||
382
.github/workflows/scripts/detect-all-changes.sh
vendored
Executable file
382
.github/workflows/scripts/detect-all-changes.sh
vendored
Executable file
@@ -0,0 +1,382 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
|
||||
# Detect what components need to be released based on version changes
|
||||
# Usage: ./detect-all-changes.sh
|
||||
echo "🔍 Auto-detecting version changes across all components..."
|
||||
|
||||
# Initialize outputs
|
||||
CORE_NEEDS_RELEASE="false"
|
||||
FRAMEWORK_NEEDS_RELEASE="false"
|
||||
PLUGINS_NEED_RELEASE="false"
|
||||
BIFROST_HTTP_NEEDS_RELEASE="false"
|
||||
DOCKER_NEEDS_RELEASE="false"
|
||||
CHANGED_PLUGINS="[]"
|
||||
|
||||
# Get current versions
|
||||
CORE_VERSION=$(cat core/version)
|
||||
FRAMEWORK_VERSION=$(cat framework/version)
|
||||
TRANSPORT_VERSION=$(cat transports/version)
|
||||
|
||||
echo "📦 Current versions:"
|
||||
echo " Core: $CORE_VERSION"
|
||||
echo " Framework: $FRAMEWORK_VERSION"
|
||||
echo " Transport: $TRANSPORT_VERSION"
|
||||
|
||||
START_FROM="none"
|
||||
|
||||
# Check Core
|
||||
echo ""
|
||||
echo "🔧 Checking core..."
|
||||
CORE_TAG="core/v${CORE_VERSION}"
|
||||
if git rev-parse --verify "$CORE_TAG" >/dev/null 2>&1; then
|
||||
echo " ⏭️ Tag $CORE_TAG already exists"
|
||||
else
|
||||
# Extract major.minor track from version (e.g., "1.3.55" -> "1.3", "1.4.0-prerelease1" -> "1.4")
|
||||
CORE_BASE_VERSION=$(echo "$CORE_VERSION" | sed 's/-.*$//')
|
||||
CORE_MAJOR_MINOR=$(echo "$CORE_BASE_VERSION" | cut -d. -f1,2)
|
||||
echo " 🔍 Checking track: ${CORE_MAJOR_MINOR}.x"
|
||||
|
||||
# Get previous version in the same track
|
||||
LATEST_CORE_TAG=$(git tag -l "core/v${CORE_MAJOR_MINOR}.*" | sort -V | tail -1)
|
||||
echo "🏷️ Latest core tag in track ${CORE_MAJOR_MINOR}.x: $LATEST_CORE_TAG"
|
||||
if [ -z "$LATEST_CORE_TAG" ]; then
|
||||
echo " ✅ First core release in track ${CORE_MAJOR_MINOR}.x: $CORE_VERSION"
|
||||
CORE_NEEDS_RELEASE="true"
|
||||
else
|
||||
if [[ "$CORE_VERSION" == *"-"* ]]; then
|
||||
# current_version has prerelease, so include all versions but prefer stable
|
||||
ALL_TAGS=$(git tag -l "core/v${CORE_MAJOR_MINOR}.*" | sort -V)
|
||||
STABLE_TAGS=$(echo "$ALL_TAGS" | grep -v '\-' || true)
|
||||
PRERELEASE_TAGS=$(echo "$ALL_TAGS" | grep '\-' || true)
|
||||
if [ -n "$STABLE_TAGS" ]; then
|
||||
# Get the highest stable version
|
||||
LATEST_CORE_TAG=$(echo "$STABLE_TAGS" | tail -1)
|
||||
echo "latest core tag (stable preferred): $LATEST_CORE_TAG"
|
||||
else
|
||||
# No stable versions, get highest prerelease
|
||||
LATEST_CORE_TAG=$(echo "$PRERELEASE_TAGS" | tail -1)
|
||||
echo "latest core tag (prerelease only): $LATEST_CORE_TAG"
|
||||
fi
|
||||
else
|
||||
# VERSION has no prerelease, so only consider stable releases in same track
|
||||
LATEST_CORE_TAG=$(git tag -l "core/v${CORE_MAJOR_MINOR}.*" | grep -v '\-' | sort -V | tail -1 || true)
|
||||
echo "latest core tag (stable only): $LATEST_CORE_TAG"
|
||||
fi
|
||||
PREVIOUS_CORE_VERSION=${LATEST_CORE_TAG#core/v}
|
||||
echo " 📋 Previous: $PREVIOUS_CORE_VERSION, Current: $CORE_VERSION"
|
||||
# Fixed: Use head -1 instead of tail -1 for your sort -V behavior, and check against current version
|
||||
if [ "$(printf '%s\n' "$PREVIOUS_CORE_VERSION" "$CORE_VERSION" | sort -V | tail -1)" = "$CORE_VERSION" ] && [ "$PREVIOUS_CORE_VERSION" != "$CORE_VERSION" ]; then
|
||||
echo " ✅ Core version incremented: $PREVIOUS_CORE_VERSION → $CORE_VERSION"
|
||||
CORE_NEEDS_RELEASE="true"
|
||||
else
|
||||
echo " ⏭️ No core version increment"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check Framework
|
||||
echo ""
|
||||
echo "📦 Checking framework..."
|
||||
FRAMEWORK_TAG="framework/v${FRAMEWORK_VERSION}"
|
||||
if git rev-parse --verify "$FRAMEWORK_TAG" >/dev/null 2>&1; then
|
||||
echo " ⏭️ Tag $FRAMEWORK_TAG already exists"
|
||||
else
|
||||
# Extract major.minor track from version (e.g., "1.3.55" -> "1.3", "1.4.0-prerelease1" -> "1.4")
|
||||
FRAMEWORK_BASE_VERSION=$(echo "$FRAMEWORK_VERSION" | sed 's/-.*$//')
|
||||
FRAMEWORK_MAJOR_MINOR=$(echo "$FRAMEWORK_BASE_VERSION" | cut -d. -f1,2)
|
||||
echo " 🔍 Checking track: ${FRAMEWORK_MAJOR_MINOR}.x"
|
||||
|
||||
LATEST_FRAMEWORK_TAG=""
|
||||
if [[ "$FRAMEWORK_VERSION" == *"-"* ]]; then
|
||||
# current_version has prerelease, so include all versions but prefer stable
|
||||
ALL_TAGS=$(git tag -l "framework/v${FRAMEWORK_MAJOR_MINOR}.*" | sort -V)
|
||||
STABLE_TAGS=$(echo "$ALL_TAGS" | grep -v '\-' || true)
|
||||
PRERELEASE_TAGS=$(echo "$ALL_TAGS" | grep '\-' || true)
|
||||
if [ -n "$STABLE_TAGS" ]; then
|
||||
# Get the highest stable version
|
||||
LATEST_FRAMEWORK_TAG=$(echo "$STABLE_TAGS" | tail -1)
|
||||
echo "latest framework tag (stable preferred): $LATEST_FRAMEWORK_TAG"
|
||||
else
|
||||
# No stable versions, get highest prerelease
|
||||
LATEST_FRAMEWORK_TAG=$(echo "$PRERELEASE_TAGS" | tail -1)
|
||||
echo "latest framework tag (prerelease only): $LATEST_FRAMEWORK_TAG"
|
||||
fi
|
||||
else
|
||||
# VERSION has no prerelease, so only consider stable releases in same track
|
||||
LATEST_FRAMEWORK_TAG=$(git tag -l "framework/v${FRAMEWORK_MAJOR_MINOR}.*" | grep -v '\-' | sort -V | tail -1 || true)
|
||||
echo "latest framework tag (stable only): $LATEST_FRAMEWORK_TAG"
|
||||
fi
|
||||
if [ -z "$LATEST_FRAMEWORK_TAG" ]; then
|
||||
echo " ✅ First framework release in track ${FRAMEWORK_MAJOR_MINOR}.x: $FRAMEWORK_VERSION"
|
||||
FRAMEWORK_NEEDS_RELEASE="true"
|
||||
else
|
||||
PREVIOUS_FRAMEWORK_VERSION=${LATEST_FRAMEWORK_TAG#framework/v}
|
||||
echo " 📋 Previous: $PREVIOUS_FRAMEWORK_VERSION, Current: $FRAMEWORK_VERSION"
|
||||
# Fixed: Use head -1 instead of tail -1 for your sort -V behavior, and check against current version
|
||||
if [ "$(printf '%s\n' "$PREVIOUS_FRAMEWORK_VERSION" "$FRAMEWORK_VERSION" | sort -V | tail -1)" = "$FRAMEWORK_VERSION" ] && [ "$PREVIOUS_FRAMEWORK_VERSION" != "$FRAMEWORK_VERSION" ]; then
|
||||
echo " ✅ Framework version incremented: $PREVIOUS_FRAMEWORK_VERSION → $FRAMEWORK_VERSION"
|
||||
FRAMEWORK_NEEDS_RELEASE="true"
|
||||
else
|
||||
echo " ⏭️ No framework version increment"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check Plugins
|
||||
echo ""
|
||||
echo "🔌 Checking plugins..."
|
||||
PLUGIN_CHANGES=()
|
||||
|
||||
for plugin_dir in plugins/*/; do
|
||||
if [ ! -d "$plugin_dir" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
plugin_name=$(basename "$plugin_dir")
|
||||
version_file="${plugin_dir}version"
|
||||
|
||||
if [ ! -f "$version_file" ]; then
|
||||
echo " ⚠️ No version file for: $plugin_name"
|
||||
continue
|
||||
fi
|
||||
|
||||
current_version=$(cat "$version_file" | tr -d '\n\r')
|
||||
if [ -z "$current_version" ]; then
|
||||
echo " ⚠️ Empty version file for: $plugin_name"
|
||||
continue
|
||||
fi
|
||||
|
||||
tag_name="plugins/${plugin_name}/v${current_version}"
|
||||
echo " 📦 Plugin: $plugin_name (v$current_version)"
|
||||
|
||||
if git rev-parse --verify "$tag_name" >/dev/null 2>&1; then
|
||||
echo " ⏭️ Tag already exists"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract major.minor track from version (e.g., "1.3.55" -> "1.3", "1.4.0-prerelease1" -> "1.4")
|
||||
plugin_base_version=$(echo "$current_version" | sed 's/-.*$//')
|
||||
plugin_major_minor=$(echo "$plugin_base_version" | cut -d. -f1,2)
|
||||
echo " 🔍 Checking track: ${plugin_major_minor}.x"
|
||||
|
||||
if [[ "$current_version" == *"-"* ]]; then
|
||||
# current_version has prerelease, so include all versions but prefer stable
|
||||
ALL_TAGS=$(git tag -l "plugins/${plugin_name}/v${plugin_major_minor}.*" | sort -V)
|
||||
STABLE_TAGS=$(echo "$ALL_TAGS" | grep -v '\-' || true)
|
||||
PRERELEASE_TAGS=$(echo "$ALL_TAGS" | grep '\-' || true)
|
||||
|
||||
if [ -n "$STABLE_TAGS" ]; then
|
||||
# Get the highest stable version
|
||||
LATEST_PLUGIN_TAG=$(echo "$STABLE_TAGS" | tail -1)
|
||||
echo "latest plugin tag (stable preferred): $LATEST_PLUGIN_TAG"
|
||||
else
|
||||
# No stable versions, get highest prerelease
|
||||
LATEST_PLUGIN_TAG=$(echo "$PRERELEASE_TAGS" | tail -1)
|
||||
echo "latest plugin tag (prerelease only): $LATEST_PLUGIN_TAG"
|
||||
fi
|
||||
else
|
||||
# VERSION has no prerelease, so only consider stable releases in same track
|
||||
LATEST_PLUGIN_TAG=$(git tag -l "plugins/${plugin_name}/v${plugin_major_minor}.*" | grep -v '\-' | sort -V | tail -1 || true)
|
||||
echo "latest plugin tag (stable only): $LATEST_PLUGIN_TAG"
|
||||
fi
|
||||
|
||||
latest_tag=$LATEST_PLUGIN_TAG
|
||||
if [ -z "$latest_tag" ]; then
|
||||
echo " ✅ First release in track ${plugin_major_minor}.x"
|
||||
PLUGIN_CHANGES+=("$plugin_name")
|
||||
else
|
||||
previous_version=${latest_tag#plugins/${plugin_name}/v}
|
||||
echo "previous version: $previous_version"
|
||||
echo "current version: $current_version"
|
||||
echo "latest tag: $latest_tag"
|
||||
if [ "$(printf '%s\n' "$previous_version" "$current_version" | sort -V | tail -1)" = "$current_version" ] && [ "$previous_version" != "$current_version" ]; then
|
||||
echo " ✅ Version incremented: $previous_version → $current_version"
|
||||
PLUGIN_CHANGES+=("$plugin_name")
|
||||
else
|
||||
echo " ⏭️ No version increment"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#PLUGIN_CHANGES[@]} -gt 0 ]; then
|
||||
PLUGINS_NEED_RELEASE="true"
|
||||
echo " 🔄 Plugins with changes: ${PLUGIN_CHANGES[*]}"
|
||||
else
|
||||
echo " ⏭️ No plugin changes detected"
|
||||
fi
|
||||
|
||||
# Check Bifrost HTTP
|
||||
echo ""
|
||||
echo "🚀 Checking bifrost-http..."
|
||||
TRANSPORT_TAG="transports/v${TRANSPORT_VERSION}"
|
||||
DOCKER_TAG_EXISTS="false"
|
||||
|
||||
# Check if Git tag exists
|
||||
GIT_TAG_EXISTS="false"
|
||||
if git rev-parse --verify "$TRANSPORT_TAG" >/dev/null 2>&1; then
|
||||
echo " ⏭️ Git tag $TRANSPORT_TAG already exists"
|
||||
GIT_TAG_EXISTS="true"
|
||||
fi
|
||||
|
||||
# Check if Docker tag exists on DockerHub
|
||||
echo " 🐳 Checking DockerHub for tag v${TRANSPORT_VERSION}..."
|
||||
DOCKER_CHECK_RESPONSE=$(curl -s "https://registry.hub.docker.com/v2/repositories/maximhq/bifrost/tags/v${TRANSPORT_VERSION}/" 2>/dev/null || echo "")
|
||||
if [ -n "$DOCKER_CHECK_RESPONSE" ] && echo "$DOCKER_CHECK_RESPONSE" | grep -q '"name"'; then
|
||||
echo " ⏭️ Docker tag v${TRANSPORT_VERSION} already exists on DockerHub"
|
||||
DOCKER_TAG_EXISTS="true"
|
||||
else
|
||||
echo " ❌ Docker tag v${TRANSPORT_VERSION} not found on DockerHub"
|
||||
fi
|
||||
|
||||
# Determine if release is needed
|
||||
if [ "$GIT_TAG_EXISTS" = "true" ] && [ "$DOCKER_TAG_EXISTS" = "true" ]; then
|
||||
echo " ⏭️ Both Git tag and Docker image exist - no release needed"
|
||||
else
|
||||
# Extract major.minor track from version (e.g., "1.3.55" -> "1.3", "1.4.0-prerelease1" -> "1.4")
|
||||
TRANSPORT_BASE_VERSION=$(echo "$TRANSPORT_VERSION" | sed 's/-.*$//')
|
||||
TRANSPORT_MAJOR_MINOR=$(echo "$TRANSPORT_BASE_VERSION" | cut -d. -f1,2)
|
||||
echo " 🔍 Checking track: ${TRANSPORT_MAJOR_MINOR}.x"
|
||||
|
||||
# Get all transport tags in the same track, prioritize stable over prerelease for same base version
|
||||
ALL_TRANSPORT_TAGS=$(git tag -l "transports/v${TRANSPORT_MAJOR_MINOR}.*" | sort -V)
|
||||
|
||||
# Function to get base version (remove prerelease suffix)
|
||||
get_base_version() {
|
||||
echo "$1" | sed 's/-.*$//'
|
||||
}
|
||||
|
||||
# Find the latest version, prioritizing stable over prerelease
|
||||
LATEST_TRANSPORT_TAG=""
|
||||
LATEST_BASE_VERSION=""
|
||||
|
||||
for tag in $ALL_TRANSPORT_TAGS; do
|
||||
version=${tag#transports/v}
|
||||
base_version=$(get_base_version "$version")
|
||||
|
||||
# If this base version is newer, or same base version but current is stable and we had prerelease
|
||||
if [ -z "$LATEST_BASE_VERSION" ] || \
|
||||
[ "$(printf '%s\n' "$LATEST_BASE_VERSION" "$base_version" | sort -V | tail -1)" = "$base_version" ]; then
|
||||
|
||||
if [ "$base_version" = "$LATEST_BASE_VERSION" ]; then
|
||||
# Same base version - prefer stable (no hyphen) over prerelease, otherwise take the later one
|
||||
if [[ "$version" != *"-"* ]]; then
|
||||
# Current is stable, always prefer it
|
||||
LATEST_TRANSPORT_TAG="$tag"
|
||||
elif [[ "${LATEST_TRANSPORT_TAG#transports/v}" == *"-"* ]]; then
|
||||
# Both are prereleases, take the later one (thanks to sort -V)
|
||||
LATEST_TRANSPORT_TAG="$tag"
|
||||
fi
|
||||
else
|
||||
# New base version is higher
|
||||
LATEST_TRANSPORT_TAG="$tag"
|
||||
LATEST_BASE_VERSION="$base_version"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$LATEST_TRANSPORT_TAG" ]; then
|
||||
echo " 🏷️ Latest transport tag: $LATEST_TRANSPORT_TAG"
|
||||
fi
|
||||
if [ -z "$LATEST_TRANSPORT_TAG" ]; then
|
||||
echo " ✅ First transport release in track ${TRANSPORT_MAJOR_MINOR}.x: $TRANSPORT_VERSION"
|
||||
if [ "$GIT_TAG_EXISTS" = "false" ]; then
|
||||
echo " 🏷️ Git tag missing - transport release needed"
|
||||
BIFROST_HTTP_NEEDS_RELEASE="true"
|
||||
fi
|
||||
else
|
||||
PREVIOUS_TRANSPORT_VERSION=${LATEST_TRANSPORT_TAG#transports/v}
|
||||
echo " 📋 Previous: $PREVIOUS_TRANSPORT_VERSION, Current: $TRANSPORT_VERSION"
|
||||
|
||||
# Function to compare versions with proper prerelease handling
|
||||
# Returns 0 if $1 < $2, 1 otherwise
|
||||
version_less_than() {
|
||||
local v1="$1"
|
||||
local v2="$2"
|
||||
|
||||
# Extract base versions (remove prerelease suffix)
|
||||
local base1=$(echo "$v1" | sed 's/-.*$//')
|
||||
local base2=$(echo "$v2" | sed 's/-.*$//')
|
||||
|
||||
# Compare base versions
|
||||
if [ "$base1" != "$base2" ]; then
|
||||
# Different base versions, use sort -V
|
||||
[ "$(printf '%s\n' "$base1" "$base2" | sort -V | head -1)" = "$base1" ]
|
||||
return $?
|
||||
fi
|
||||
|
||||
# Same base version, check prereleases
|
||||
local pre1=$(echo "$v1" | grep -o '\-.*$' || echo "")
|
||||
local pre2=$(echo "$v2" | grep -o '\-.*$' || echo "")
|
||||
|
||||
if [ -z "$pre1" ] && [ -n "$pre2" ]; then
|
||||
# v1 is stable, v2 is prerelease: v2 < v1
|
||||
return 1
|
||||
elif [ -n "$pre1" ] && [ -z "$pre2" ]; then
|
||||
# v1 is prerelease, v2 is stable: v1 < v2
|
||||
return 0
|
||||
elif [ -n "$pre1" ] && [ -n "$pre2" ]; then
|
||||
# Both prereleases, compare them
|
||||
[ "$(printf '%s\n' "$pre1" "$pre2" | sort -V | head -1)" = "$pre1" ]
|
||||
return $?
|
||||
else
|
||||
# Both stable and same base: equal
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if current version is greater than previous
|
||||
if version_less_than "$PREVIOUS_TRANSPORT_VERSION" "$TRANSPORT_VERSION"; then
|
||||
echo " ✅ Transport version incremented: $PREVIOUS_TRANSPORT_VERSION → $TRANSPORT_VERSION"
|
||||
if [ "$GIT_TAG_EXISTS" = "false" ]; then
|
||||
echo " 🏷️ Git tag missing - transport release needed"
|
||||
BIFROST_HTTP_NEEDS_RELEASE="true"
|
||||
fi
|
||||
else
|
||||
echo " ⏭️ No transport version increment"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if Docker image needs to be built (independent of transport release)
|
||||
if [ "$DOCKER_TAG_EXISTS" = "false" ]; then
|
||||
echo " 🐳 Docker image missing - docker release needed"
|
||||
DOCKER_NEEDS_RELEASE="true"
|
||||
fi
|
||||
|
||||
|
||||
# Convert plugin array to JSON (compact format)
|
||||
if [ ${#PLUGIN_CHANGES[@]} -eq 0 ]; then
|
||||
CHANGED_PLUGINS_JSON="[]"
|
||||
else
|
||||
CHANGED_PLUGINS_JSON=$(printf '%s\n' "${PLUGIN_CHANGES[@]}" | jq -R . | jq -s -c .)
|
||||
fi
|
||||
|
||||
echo "CHANGED_PLUGINS_JSON: $CHANGED_PLUGINS_JSON"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "📋 Release Summary:"
|
||||
echo " Core: $CORE_NEEDS_RELEASE (v$CORE_VERSION)"
|
||||
echo " Framework: $FRAMEWORK_NEEDS_RELEASE (v$FRAMEWORK_VERSION)"
|
||||
echo " Plugins: $PLUGINS_NEED_RELEASE (${#PLUGIN_CHANGES[@]} plugins)"
|
||||
echo " Bifrost HTTP: $BIFROST_HTTP_NEEDS_RELEASE (v$TRANSPORT_VERSION)"
|
||||
echo " Docker: $DOCKER_NEEDS_RELEASE (v$TRANSPORT_VERSION)"
|
||||
|
||||
# Set outputs (only when running in GitHub Actions)
|
||||
if [ -n "${GITHUB_OUTPUT:-}" ]; then
|
||||
{
|
||||
echo "core-needs-release=$CORE_NEEDS_RELEASE"
|
||||
echo "framework-needs-release=$FRAMEWORK_NEEDS_RELEASE"
|
||||
echo "plugins-need-release=$PLUGINS_NEED_RELEASE"
|
||||
echo "bifrost-http-needs-release=$BIFROST_HTTP_NEEDS_RELEASE"
|
||||
echo "docker-needs-release=$DOCKER_NEEDS_RELEASE"
|
||||
echo "changed-plugins=$CHANGED_PLUGINS_JSON"
|
||||
echo "core-version=$CORE_VERSION"
|
||||
echo "framework-version=$FRAMEWORK_VERSION"
|
||||
echo "transport-version=$TRANSPORT_VERSION"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ℹ️ GITHUB_OUTPUT not set; skipping outputs write (local run)"
|
||||
fi
|
||||
41
.github/workflows/scripts/extract-npx-version.sh
vendored
Executable file
41
.github/workflows/scripts/extract-npx-version.sh
vendored
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Extract NPX version from package.json
|
||||
# Usage: ./extract-npx-version.sh
|
||||
|
||||
# Path to package.json
|
||||
PACKAGE_JSON="npx/bifrost/package.json"
|
||||
|
||||
if [[ ! -f "${PACKAGE_JSON}" ]]; then
|
||||
echo "❌ package.json not found at ${PACKAGE_JSON}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📋 Reading version from ${PACKAGE_JSON}"
|
||||
|
||||
# Extract version from package.json using jq
|
||||
VERSION=$(jq -r '.version' "${PACKAGE_JSON}")
|
||||
|
||||
if [[ -z "${VERSION}" ]] || [[ "${VERSION}" == "null" ]]; then
|
||||
echo "❌ Failed to extract version from package.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate version format (X.Y.Z or prerelease like X.Y.Z-rc.1)
|
||||
if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
|
||||
echo "❌ Invalid version format '${VERSION}'. Expected format: MAJOR.MINOR.PATCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📦 Extracted NPX version: ${VERSION}"
|
||||
|
||||
# Set outputs (only when running in GitHub Actions)
|
||||
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
|
||||
{
|
||||
echo "version=${VERSION}"
|
||||
echo "full-tag=npx/bifrost/v${VERSION}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::notice::GITHUB_OUTPUT not set; skipping outputs (local run?)"
|
||||
fi
|
||||
72
.github/workflows/scripts/get_curls.sh
vendored
Executable file
72
.github/workflows/scripts/get_curls.sh
vendored
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
set -uo pipefail
|
||||
|
||||
# Bifrost HTTP Transport - GET API Endpoints
|
||||
# This script tests all GET endpoints and reports their status
|
||||
|
||||
# Base URL (update as needed)
|
||||
BASE_URL="${BASE_URL:-http://localhost:8080}"
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Track failures
|
||||
FAILED_TESTS=0
|
||||
TOTAL_TESTS=0
|
||||
|
||||
echo "Bifrost GET API Endpoints - Status Check"
|
||||
echo "========================================"
|
||||
echo "Base URL: $BASE_URL"
|
||||
echo ""
|
||||
|
||||
# Function to test endpoint
|
||||
test_endpoint() {
|
||||
local path=$1
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||
local status=$(curl -s -o /dev/null -w "%{http_code}" -X GET "$BASE_URL$path" -H "Content-Type: application/json")
|
||||
|
||||
if [ "$status" -ge 200 ] && [ "$status" -lt 300 ]; then
|
||||
echo -e "GET $path - ${GREEN}✓ SUCCESS${NC} ($status)"
|
||||
else
|
||||
echo -e "GET $path - ${RED}✗ FAILURE${NC} ($status)"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# Test all endpoints
|
||||
test_endpoint "/health"
|
||||
test_endpoint "/api/session/is-auth-enabled"
|
||||
test_endpoint "/api/plugins"
|
||||
test_endpoint "/api/plugins/telemetry"
|
||||
test_endpoint "/api/mcp/clients"
|
||||
test_endpoint "/api/logs?limit=10&offset=0&sort_by=timestamp&order=desc"
|
||||
test_endpoint "/api/logs/dropped"
|
||||
test_endpoint "/api/logs/filterdata"
|
||||
test_endpoint "/api/providers"
|
||||
test_endpoint "/api/providers/openai"
|
||||
test_endpoint "/api/keys"
|
||||
test_endpoint "/api/governance/virtual-keys"
|
||||
test_endpoint "/api/governance/virtual-keys/vk-123"
|
||||
test_endpoint "/api/governance/teams"
|
||||
test_endpoint "/api/governance/teams/team-123"
|
||||
test_endpoint "/api/governance/customers"
|
||||
test_endpoint "/api/governance/customers/cust-123"
|
||||
test_endpoint "/api/config"
|
||||
test_endpoint "/api/config?from_db=true"
|
||||
test_endpoint "/api/version"
|
||||
test_endpoint "/v1/models"
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}Note: WebSocket endpoint (/ws) requires a WebSocket client${NC}"
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Test Summary:"
|
||||
echo " Total tests: $TOTAL_TESTS"
|
||||
echo " Passed: $((TOTAL_TESTS - FAILED_TESTS))"
|
||||
echo " Failed: $FAILED_TESTS"
|
||||
echo "========================================"
|
||||
|
||||
echo "The aim of the script is to make sure bifrost server is not crashing"
|
||||
45
.github/workflows/scripts/go-utils.sh
vendored
Executable file
45
.github/workflows/scripts/go-utils.sh
vendored
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Shared utilities for Go operations in release scripts
|
||||
# Usage: source .github/workflows/scripts/go-utils.sh
|
||||
|
||||
# Function to perform go get with exponential backoff
|
||||
# Usage: go_get_with_backoff <package@version>
|
||||
go_get_with_backoff() {
|
||||
local package="$1"
|
||||
local max_attempts=30
|
||||
local initial_wait=30
|
||||
local max_wait=120 # 2 minutes
|
||||
local attempt=1
|
||||
local wait_time=$initial_wait
|
||||
|
||||
echo "🔄 Attempting to get $package with exponential backoff..."
|
||||
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
echo "📦 Attempt $attempt/$max_attempts: go get $package"
|
||||
|
||||
if go get "$package"; then
|
||||
echo "✅ Successfully retrieved $package on attempt $attempt"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ $attempt -eq $max_attempts ]; then
|
||||
echo "❌ Failed to get $package after $max_attempts attempts"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "⏳ Waiting ${wait_time}s before retry (attempt $attempt/$max_attempts failed)..."
|
||||
sleep $wait_time
|
||||
|
||||
# Calculate next wait time (exponential backoff)
|
||||
# Double the wait time, but cap at max_wait
|
||||
wait_time=$((wait_time * 2))
|
||||
if [ $wait_time -gt $max_wait ]; then
|
||||
wait_time=$max_wait
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
81
.github/workflows/scripts/install-cross-compilers.sh
vendored
Executable file
81
.github/workflows/scripts/install-cross-compilers.sh
vendored
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Install cross-compilation toolchains for Go + CGO
|
||||
# Usage: ./install-cross-compilers.sh
|
||||
|
||||
echo "📦 Installing cross-compilation toolchains for Go + CGO..."
|
||||
|
||||
# Install all required packages
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
gcc-x86-64-linux-gnu \
|
||||
gcc-aarch64-linux-gnu \
|
||||
gcc-mingw-w64-x86-64 \
|
||||
musl-tools \
|
||||
clang \
|
||||
lld \
|
||||
xz-utils \
|
||||
curl
|
||||
|
||||
# Create symbolic links for musl compilers
|
||||
sudo ln -sf /usr/bin/x86_64-linux-gnu-gcc /usr/local/bin/x86_64-linux-musl-gcc
|
||||
sudo ln -sf /usr/bin/x86_64-linux-gnu-g++ /usr/local/bin/x86_64-linux-musl-g++
|
||||
sudo ln -sf /usr/bin/aarch64-linux-gnu-gcc /usr/local/bin/aarch64-linux-musl-gcc
|
||||
sudo ln -sf /usr/bin/aarch64-linux-gnu-g++ /usr/local/bin/aarch64-linux-musl-g++
|
||||
|
||||
echo "🍎 Setting up Darwin cross-compilation..."
|
||||
|
||||
# Where to install SDK
|
||||
SDK_DIR="/opt/MacOSX12.3.sdk"
|
||||
SDK_URL="https://github.com/phracker/MacOSX-SDKs/releases/download/12.3/MacOSX12.3.sdk.tar.xz"
|
||||
|
||||
# Download and extract macOS SDK if not already installed
|
||||
if [ ! -d "$SDK_DIR" ]; then
|
||||
echo "📦 Downloading macOS SDK..."
|
||||
# Use -f to fail on HTTP errors, -L to follow redirects
|
||||
if ! curl -fL "$SDK_URL" -o /tmp/MacOSX12.3.sdk.tar.xz; then
|
||||
echo "❌ Failed to download macOS SDK from primary URL, trying alternative..."
|
||||
SDK_URL_ALT="https://github.com/joseluisq/macosx-sdks/releases/download/12.3/MacOSX12.3.sdk.tar.xz"
|
||||
curl -fL "$SDK_URL_ALT" -o /tmp/MacOSX12.3.sdk.tar.xz
|
||||
fi
|
||||
sudo mkdir -p /opt
|
||||
sudo tar -xf /tmp/MacOSX12.3.sdk.tar.xz -C /opt
|
||||
rm -f /tmp/MacOSX12.3.sdk.tar.xz
|
||||
fi
|
||||
|
||||
# Create wrapper scripts with proper shebang and linker configuration
|
||||
sudo tee /usr/local/bin/o64-clang > /dev/null << 'WRAPPER_EOF'
|
||||
#!/bin/bash
|
||||
exec clang -target x86_64-apple-darwin --sysroot=/opt/MacOSX12.3.sdk -fuse-ld=lld -Wno-unused-command-line-argument "$@"
|
||||
WRAPPER_EOF
|
||||
|
||||
sudo tee /usr/local/bin/o64-clang++ > /dev/null << 'WRAPPER_EOF'
|
||||
#!/bin/bash
|
||||
exec clang++ -target x86_64-apple-darwin --sysroot=/opt/MacOSX12.3.sdk -fuse-ld=lld -Wno-unused-command-line-argument "$@"
|
||||
WRAPPER_EOF
|
||||
|
||||
sudo tee /usr/local/bin/oa64-clang > /dev/null << 'WRAPPER_EOF'
|
||||
#!/bin/bash
|
||||
exec clang -target arm64-apple-darwin --sysroot=/opt/MacOSX12.3.sdk -fuse-ld=lld -Wno-unused-command-line-argument "$@"
|
||||
WRAPPER_EOF
|
||||
|
||||
sudo tee /usr/local/bin/oa64-clang++ > /dev/null << 'WRAPPER_EOF'
|
||||
#!/bin/bash
|
||||
exec clang++ -target arm64-apple-darwin --sysroot=/opt/MacOSX12.3.sdk -fuse-ld=lld -Wno-unused-command-line-argument "$@"
|
||||
WRAPPER_EOF
|
||||
|
||||
sudo chmod +x /usr/local/bin/o64-clang /usr/local/bin/o64-clang++ \
|
||||
/usr/local/bin/oa64-clang /usr/local/bin/oa64-clang++
|
||||
|
||||
echo "✅ Darwin cross-compilation environment ready!"
|
||||
|
||||
echo "✅ Cross-compilation toolchains installed"
|
||||
echo ""
|
||||
echo "Available cross-compilers:"
|
||||
echo " Linux amd64: x86_64-linux-musl-gcc, x86_64-linux-musl-g++"
|
||||
echo " Linux arm64: aarch64-linux-musl-gcc, aarch64-linux-musl-g++"
|
||||
echo " Windows amd64: x86_64-w64-mingw32-gcc, x86_64-w64-mingw32-g++"
|
||||
echo " Windows arm64: aarch64-w64-mingw32-gcc, aarch64-w64-mingw32-g++"
|
||||
echo " Darwin amd64: o64-clang, o64-clang++"
|
||||
echo " Darwin arm64: oa64-clang, oa64-clang++"
|
||||
39
.github/workflows/scripts/load-test-results.json
vendored
Normal file
39
.github/workflows/scripts/load-test-results.json
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"overhead": {
|
||||
"configured_rate": 1000,
|
||||
"actual_rate": 1000,
|
||||
"duration": 30,
|
||||
"concurrent": 1000,
|
||||
"success_rate": 100.00,
|
||||
"latency_us": {
|
||||
"min": 999926.96,
|
||||
"mean": 849.31,
|
||||
"p50": 155.78,
|
||||
"p90": 408.13,
|
||||
"p95": 636.92,
|
||||
"p99": 4526.28,
|
||||
"max": 176968.29
|
||||
}
|
||||
},
|
||||
"timestamp": "2026-02-14T12:40:01Z",
|
||||
"stress": {
|
||||
"rate": 1000,
|
||||
"duration": 30,
|
||||
"mocker_latency_ms": 1000,
|
||||
"success_rate": 100.00
|
||||
},
|
||||
"process_stats": {
|
||||
"overhead": {
|
||||
"cpu_avg_pct": 20.7,
|
||||
"cpu_peak_pct": 66.9,
|
||||
"rss_avg_mb": 332.8,
|
||||
"rss_peak_mb": 640.2
|
||||
},
|
||||
"stress": {
|
||||
"cpu_avg_pct": 29.9,
|
||||
"cpu_peak_pct": 73.5,
|
||||
"rss_avg_mb": 639.5,
|
||||
"rss_peak_mb": 789.2
|
||||
}
|
||||
}
|
||||
}
|
||||
42
.github/workflows/scripts/load-test-results.md
vendored
Normal file
42
.github/workflows/scripts/load-test-results.md
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
# Bifrost Load Test Results (single instance, 1000 RPS)
|
||||
|
||||
## Bifrost Processing Overhead
|
||||
|
||||
| Metric | Actual RPS | Duration | Concurrent | Success Rate | Min | Mean | P50 | P90 | P95 | P99 | Max |
|
||||
|--------|-----------|----------|------------|--------------|-----|------|-----|-----|-----|-----|-----|
|
||||
| Overhead | 1000 | 30s | ~1000 | 100.00% | 999926.96µs | 849.31µs | 155.78µs | 408.13µs | 636.92µs | 4526.28µs | 176968.29µs |
|
||||
|
||||
## Stress #1 (1000ms mocker latency)
|
||||
|
||||
| Metric | Actual RPS | Duration | Concurrent | Success Rate | Min | Mean | P50 | P90 | P95 | P99 | Max |
|
||||
|--------|-----------|----------|------------|--------------|-----|------|-----|-----|-----|-----|-----|
|
||||
| Stress #1 | 1000 | 30s | ~1000 | 100.00% | 1000.20ms | 1002.58ms | 1000.64ms | 1001.17ms | 1001.67ms | 1047.60ms | 1286.46ms |
|
||||
|
||||
## Stress #2 (1000ms mocker latency)
|
||||
|
||||
| Metric | Actual RPS | Duration | Concurrent | Success Rate | Min | Mean | P50 | P90 | P95 | P99 | Max |
|
||||
|--------|-----------|----------|------------|--------------|-----|------|-----|-----|-----|-----|-----|
|
||||
| Stress #2 | 1000 | 30s | ~1000 | 100.00% | 1000.21ms | 1001.99ms | 1000.54ms | 1000.96ms | 1001.32ms | 1049.41ms | 1252.60ms |
|
||||
|
||||
## Bifrost Process Stats (single instance)
|
||||
|
||||
| Phase | CPU Avg | CPU Peak | RSS Avg | RSS Peak |
|
||||
|-------|---------|----------|---------|----------|
|
||||
| Overhead | 20.7% | 66.9% | 332.8MB | 640.2MB |
|
||||
| Stress | 29.9% | 73.5% | 639.5MB | 789.2MB |
|
||||
|
||||
## Method
|
||||
|
||||
- **Single instance**: All tests run against one bifrost-http process at 1000 RPS
|
||||
- **Overhead measurement**: Mocker at 1000ms latency, calibration (Vegeta->Mocker) subtracted from test (Vegeta->Bifrost->Mocker)
|
||||
- **Stress test**: Mocker at 1000ms latency, verifies 100% success under sustained concurrency
|
||||
|
||||
## Notes
|
||||
|
||||
- Overhead values are in microseconds (µs), stress test values in milliseconds (ms)
|
||||
- Overhead ignores the mocker jitter, local network request queuing. In real-world the P99 overhead will be approximately 100 microseconds.
|
||||
- Tiered overhead thresholds: mean<5000µs, p50<5000µs, p90<10000µs, p95<20000µs, p99<100000µs
|
||||
- P50/P90/P95/P99 represent percentile latencies
|
||||
|
||||
---
|
||||
*Generated by Bifrost Load Test Script*
|
||||
850
.github/workflows/scripts/load-test.sh
vendored
Executable file
850
.github/workflows/scripts/load-test.sh
vendored
Executable file
@@ -0,0 +1,850 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Load Test Script for Bifrost
|
||||
# Runs a load test against bifrost-http with a mocker provider
|
||||
# Usage: ./load-test.sh
|
||||
#
|
||||
# This script:
|
||||
# 1. Builds bifrost-http and mocker locally
|
||||
# 2. Creates a config.json with mocker provider (OpenAI-style)
|
||||
# 3. Starts mocker with 0ms latency and bifrost-http
|
||||
# 4. Runs a calibration (Vegeta -> Mocker direct) to measure Vegeta+network baseline
|
||||
# 5. Runs the overhead test (Vegeta -> Bifrost -> Mocker) to measure total
|
||||
# 6. Subtracts calibration from test to isolate Bifrost proxy overhead
|
||||
# (includes local network hop, JSON parsing/unparsing, plugins, and mocker jitter)
|
||||
# 7. Restarts mocker with 10s latency for a sustained concurrency stress test
|
||||
# 8. Asserts overhead < tiered thresholds (per percentile) and stress test has 100% success rate
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||
BIFROST_HTTP_DIR="${REPO_ROOT}/transports/bifrost-http"
|
||||
TRANSPORTS_DIR="${REPO_ROOT}/transports"
|
||||
WORK_DIR="${SCRIPT_DIR}"
|
||||
MOCKER_DIR="${REPO_ROOT}/../bifrost-benchmarking/mocker"
|
||||
|
||||
BIFROST_PORT=8080
|
||||
MOCKER_PORT=8000
|
||||
RATE=1000
|
||||
MAX_WORKERS=12000
|
||||
OVERHEAD_DURATION=30 # overhead measurement duration (seconds)
|
||||
STRESS_DURATION=30 # stress test duration (seconds)
|
||||
OVERHEAD_MOCKER_LATENCY_MS=1000 # 1 second latency for overhead measurement
|
||||
STRESS_MOCKER_LATENCY_MS=1000 # 1 second latency for stress test
|
||||
# Tiered overhead thresholds (µs) — these cover the full proxy cost:
|
||||
# local network hop, JSON parsing/unparsing, plugins, and mocker jitter.
|
||||
# At ${RATE} RPS × ${OVERHEAD_MOCKER_LATENCY_MS}ms latency ≈ 1000 concurrent requests.
|
||||
MAX_OVERHEAD_MEAN_US=5000 # mean overhead threshold (5ms)
|
||||
MAX_OVERHEAD_P50_US=5000 # p50 overhead threshold (5ms)
|
||||
MAX_OVERHEAD_P90_US=10000 # p90 overhead threshold (10ms)
|
||||
MAX_OVERHEAD_P95_US=20000 # p95 overhead threshold (20ms)
|
||||
MAX_OVERHEAD_P99_US=100000 # p99 overhead threshold (100ms)
|
||||
|
||||
# Results storage for summary table
|
||||
RESULTS_FILE="${WORK_DIR}/load-test-results.md"
|
||||
RESULTS_JSON="${WORK_DIR}/load-test-results.json"
|
||||
|
||||
# Process stats monitoring
|
||||
STATS_PID=""
|
||||
STATS_FILE="${WORK_DIR}/bifrost-stats.csv"
|
||||
|
||||
# Overhead-phase process stats (saved before bifrost restart)
|
||||
OVERHEAD_STATS_CPU_AVG=""
|
||||
OVERHEAD_STATS_CPU_PEAK=""
|
||||
OVERHEAD_STATS_RSS_AVG=""
|
||||
OVERHEAD_STATS_RSS_PEAK=""
|
||||
|
||||
# Calibration results per bucket (Vegeta -> Mocker direct)
|
||||
CAL_MIN_NS=0
|
||||
CAL_MEAN_NS=0
|
||||
CAL_50_NS=0
|
||||
CAL_90_NS=0
|
||||
CAL_95_NS=0
|
||||
CAL_99_NS=0
|
||||
CAL_MAX_NS=0
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Cleanup function to kill background processes
|
||||
cleanup() {
|
||||
log_info "Cleaning up..."
|
||||
if [ -n "$STATS_PID" ] && kill -0 "$STATS_PID" 2>/dev/null; then
|
||||
kill "$STATS_PID" 2>/dev/null || true
|
||||
wait "$STATS_PID" 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "$BIFROST_PID" ] && kill -0 "$BIFROST_PID" 2>/dev/null; then
|
||||
kill "$BIFROST_PID" 2>/dev/null || true
|
||||
wait "$BIFROST_PID" 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "$MOCKER_PID" ] && kill -0 "$MOCKER_PID" 2>/dev/null; then
|
||||
kill "$MOCKER_PID" 2>/dev/null || true
|
||||
wait "$MOCKER_PID" 2>/dev/null || true
|
||||
fi
|
||||
# Clean up temporary files (keep results files for artifact upload)
|
||||
rm -f "${WORK_DIR}/config.json" "${WORK_DIR}/logs.db" "${WORK_DIR}/attack.bin" "${WORK_DIR}/calibration.bin" "${WORK_DIR}/stress.bin" "${WORK_DIR}/bifrost.log" "${WORK_DIR}/vegeta-target.json" "${WORK_DIR}/vegeta-target-calibration.json" "${WORK_DIR}/vegeta-target-stress.json" "${WORK_DIR}/vegeta-report.json" "${WORK_DIR}/bifrost-stats.csv" 2>/dev/null || true
|
||||
log_info "Cleanup complete"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
# Check for required tools
|
||||
check_dependencies() {
|
||||
log_info "Checking dependencies..."
|
||||
|
||||
if ! command -v go &> /dev/null; then
|
||||
log_error "Go is not installed. Please install Go 1.24.3 or later."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v git &> /dev/null; then
|
||||
log_error "Git is not installed. Please install Git."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "All dependencies found"
|
||||
}
|
||||
|
||||
# Kill any process listening on a specific port (not processes with connections to it)
|
||||
kill_port() {
|
||||
local port=$1
|
||||
local pids=$(lsof -ti "TCP:${port}" -sTCP:LISTEN 2>/dev/null)
|
||||
if [ -n "$pids" ]; then
|
||||
log_warn "Killing existing process(es) listening on port ${port}: ${pids}"
|
||||
echo "$pids" | xargs kill -9 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Kill processes on required ports before starting
|
||||
cleanup_ports() {
|
||||
log_info "Checking for processes on required ports..."
|
||||
kill_port ${MOCKER_PORT}
|
||||
kill_port ${BIFROST_PORT}
|
||||
}
|
||||
|
||||
# Install Vegeta if not present
|
||||
install_vegeta() {
|
||||
if ! command -v vegeta &> /dev/null; then
|
||||
log_info "Installing Vegeta load testing tool..."
|
||||
go install github.com/tsenart/vegeta/v12@latest
|
||||
export PATH="$PATH:$(go env GOPATH)/bin"
|
||||
if ! command -v vegeta &> /dev/null; then
|
||||
log_error "Failed to install Vegeta"
|
||||
exit 1
|
||||
fi
|
||||
log_success "Vegeta installed"
|
||||
else
|
||||
log_success "Vegeta already installed"
|
||||
fi
|
||||
}
|
||||
|
||||
# Build bifrost-http if binary doesn't exist
|
||||
build_bifrost_http() {
|
||||
if [ -f "${REPO_ROOT}/tmp/bifrost-http" ]; then
|
||||
log_success "bifrost-http binary already exists at ${REPO_ROOT}/tmp/bifrost-http"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Building bifrost-http..."
|
||||
cd "${TRANSPORTS_DIR}"
|
||||
|
||||
if go build -o ${REPO_ROOT}/tmp/bifrost-http .; then
|
||||
log_success "bifrost-http built successfully"
|
||||
else
|
||||
log_error "Failed to build bifrost-http"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "${WORK_DIR}"
|
||||
}
|
||||
|
||||
# Clone and setup mocker from bifrost-benchmarking
|
||||
setup_mocker() {
|
||||
if [ -d "${REPO_ROOT}/../bifrost-benchmarking" ]; then
|
||||
log_info "Updating bifrost-benchmarking repository..."
|
||||
cd "${REPO_ROOT}/../bifrost-benchmarking"
|
||||
git pull --quiet || true
|
||||
cd "${WORK_DIR}"
|
||||
else
|
||||
log_info "Cloning bifrost-benchmarking repository..."
|
||||
cd "${WORK_DIR}"
|
||||
git clone --depth 1 https://github.com/maximhq/bifrost-benchmarking.git
|
||||
fi
|
||||
|
||||
log_success "Mocker setup complete"
|
||||
}
|
||||
|
||||
# Build mocker binary (avoids go run overhead)
|
||||
build_mocker() {
|
||||
if [ -f "${REPO_ROOT}/tmp/mocker" ]; then
|
||||
log_success "mocker binary already exists at ${REPO_ROOT}/tmp/mocker"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Building mocker..."
|
||||
cd "${MOCKER_DIR}"
|
||||
|
||||
if go build -o "${REPO_ROOT}/tmp/mocker" .; then
|
||||
log_success "mocker built successfully"
|
||||
else
|
||||
log_error "Failed to build mocker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "${WORK_DIR}"
|
||||
}
|
||||
|
||||
# Create config.json for bifrost with mocker provider
|
||||
create_config() {
|
||||
log_info "Creating config.json..."
|
||||
|
||||
cat > "${WORK_DIR}/config.json" << 'EOF'
|
||||
{
|
||||
"$schema": "https://www.getbifrost.ai/schema",
|
||||
"client": {
|
||||
"enable_logging": false,
|
||||
"initial_pool_size": 20000,
|
||||
"drop_excess_requests": false,
|
||||
"allow_direct_keys": false
|
||||
},
|
||||
"config_store": {
|
||||
"enabled": false
|
||||
},
|
||||
"logs_store": {
|
||||
"enabled": false
|
||||
},
|
||||
"providers": {
|
||||
"openai": {
|
||||
"keys": [
|
||||
{
|
||||
"name": "mocker-key",
|
||||
"value": "Bearer mocker-key",
|
||||
"weight": 1
|
||||
}
|
||||
],
|
||||
"network_config": {
|
||||
"base_url": "http://localhost:8000",
|
||||
"default_request_timeout_in_seconds": 30
|
||||
},
|
||||
"concurrency_and_buffer_size": {
|
||||
"concurrency": 20000,
|
||||
"buffer_size": 40000
|
||||
},
|
||||
"custom_provider_config": {
|
||||
"base_provider_type": "openai",
|
||||
"allowed_requests": {
|
||||
"list_models": false,
|
||||
"chat_completion": true,
|
||||
"chat_completion_stream": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
log_success "config.json created"
|
||||
}
|
||||
|
||||
# Start mocker with specified latency
|
||||
# Arguments: $1 = latency in ms
|
||||
start_mocker() {
|
||||
local latency_ms=${1:-0}
|
||||
log_info "Starting mocker server on port ${MOCKER_PORT} with ${latency_ms}ms latency..."
|
||||
|
||||
"${REPO_ROOT}/tmp/mocker" -port ${MOCKER_PORT} -host 0.0.0.0 -latency ${latency_ms} &
|
||||
MOCKER_PID=$!
|
||||
|
||||
# Wait for mocker to be ready
|
||||
local max_attempts=30
|
||||
local attempt=0
|
||||
while ! curl -s "http://localhost:${MOCKER_PORT}/v1/chat/completions" -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer mocker-key" \
|
||||
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"test"}]}' > /dev/null 2>&1; do
|
||||
sleep 1
|
||||
attempt=$((attempt + 1))
|
||||
if [ $attempt -ge $max_attempts ]; then
|
||||
log_error "Mocker failed to start within ${max_attempts} seconds"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
log_success "Mocker server started (PID: ${MOCKER_PID})"
|
||||
}
|
||||
|
||||
# Stop mocker
|
||||
stop_mocker() {
|
||||
if [ -n "$MOCKER_PID" ] && kill -0 "$MOCKER_PID" 2>/dev/null; then
|
||||
log_info "Stopping mocker (PID: ${MOCKER_PID})..."
|
||||
kill "$MOCKER_PID" 2>/dev/null || true
|
||||
wait "$MOCKER_PID" 2>/dev/null || true
|
||||
MOCKER_PID=""
|
||||
sleep 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Stop bifrost-http server
|
||||
stop_bifrost() {
|
||||
if [ -n "$BIFROST_PID" ] && kill -0 "$BIFROST_PID" 2>/dev/null; then
|
||||
log_info "Stopping bifrost (PID: ${BIFROST_PID})..."
|
||||
kill "$BIFROST_PID" 2>/dev/null || true
|
||||
wait "$BIFROST_PID" 2>/dev/null || true
|
||||
BIFROST_PID=""
|
||||
sleep 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Start background process stats collection for bifrost
|
||||
# Samples CPU% and RSS every second, writes to CSV
|
||||
start_stats_monitor() {
|
||||
if [ -z "$BIFROST_PID" ] || ! kill -0 "$BIFROST_PID" 2>/dev/null; then
|
||||
log_warn "Cannot start stats monitor: bifrost not running"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "timestamp,cpu_pct,rss_mb" > "${STATS_FILE}"
|
||||
|
||||
(
|
||||
while kill -0 "$BIFROST_PID" 2>/dev/null; do
|
||||
# ps -o %cpu= -o rss= works on both macOS and Linux
|
||||
stats=$(ps -p "$BIFROST_PID" -o %cpu=,rss= 2>/dev/null)
|
||||
if [ -n "$stats" ]; then
|
||||
cpu=$(echo "$stats" | awk '{print $1}')
|
||||
rss_kb=$(echo "$stats" | awk '{print $2}')
|
||||
rss_mb=$(echo "scale=1; ${rss_kb} / 1024" | bc)
|
||||
echo "$(date +%s),${cpu},${rss_mb}" >> "${STATS_FILE}"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
) &
|
||||
STATS_PID=$!
|
||||
log_info "Stats monitor started (PID: ${STATS_PID})"
|
||||
}
|
||||
|
||||
# Stop stats monitor and print summary
|
||||
stop_stats_monitor() {
|
||||
if [ -n "$STATS_PID" ] && kill -0 "$STATS_PID" 2>/dev/null; then
|
||||
kill "$STATS_PID" 2>/dev/null || true
|
||||
wait "$STATS_PID" 2>/dev/null || true
|
||||
STATS_PID=""
|
||||
fi
|
||||
|
||||
if [ ! -f "${STATS_FILE}" ] || [ $(wc -l < "${STATS_FILE}") -le 1 ]; then
|
||||
log_warn "No process stats collected"
|
||||
return
|
||||
fi
|
||||
|
||||
# Compute peak and average CPU/RSS from CSV (skip header)
|
||||
if command -v awk &> /dev/null; then
|
||||
local stats_summary=$(awk -F',' 'NR>1 {
|
||||
cpu_sum+=$2; rss_sum+=$3; n++;
|
||||
if($2>cpu_max) cpu_max=$2;
|
||||
if($3>rss_max) rss_max=$3;
|
||||
} END {
|
||||
if(n>0) printf "%.1f,%.1f,%.1f,%.1f,%d", cpu_sum/n, cpu_max, rss_sum/n, rss_max, n
|
||||
}' "${STATS_FILE}")
|
||||
|
||||
STATS_CPU_AVG=$(echo "$stats_summary" | cut -d',' -f1)
|
||||
STATS_CPU_PEAK=$(echo "$stats_summary" | cut -d',' -f2)
|
||||
STATS_RSS_AVG=$(echo "$stats_summary" | cut -d',' -f3)
|
||||
STATS_RSS_PEAK=$(echo "$stats_summary" | cut -d',' -f4)
|
||||
local samples=$(echo "$stats_summary" | cut -d',' -f5)
|
||||
|
||||
echo ""
|
||||
log_success "Bifrost process stats (single instance, ${samples} samples):"
|
||||
log_info " CPU: avg=${STATS_CPU_AVG}%, peak=${STATS_CPU_PEAK}%"
|
||||
log_info " RSS: avg=${STATS_RSS_AVG}MB, peak=${STATS_RSS_PEAK}MB"
|
||||
fi
|
||||
}
|
||||
|
||||
# Start bifrost-http server
|
||||
start_bifrost() {
|
||||
log_info "Starting bifrost-http on port ${BIFROST_PORT}..."
|
||||
|
||||
cd "${WORK_DIR}"
|
||||
local bifrost_log="${WORK_DIR}/bifrost.log"
|
||||
"${REPO_ROOT}/tmp/bifrost-http" -app-dir "${WORK_DIR}" -port "${BIFROST_PORT}" -host "0.0.0.0" -log-level "info" > "${bifrost_log}" 2>&1 &
|
||||
BIFROST_PID=$!
|
||||
|
||||
# Wait for bifrost to be fully ready (look for "successfully started bifrost" message)
|
||||
local max_attempts=60
|
||||
local attempt=0
|
||||
while ! grep -q "successfully started bifrost" "${bifrost_log}" 2>/dev/null; do
|
||||
sleep 1
|
||||
attempt=$((attempt + 1))
|
||||
if [ $attempt -ge $max_attempts ]; then
|
||||
log_error "Bifrost failed to start within ${max_attempts} seconds"
|
||||
log_error "Bifrost log output:"
|
||||
cat "${bifrost_log}" 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
# Check if process is still running
|
||||
if ! kill -0 "$BIFROST_PID" 2>/dev/null; then
|
||||
log_error "Bifrost process died unexpectedly"
|
||||
log_error "Bifrost log output:"
|
||||
cat "${bifrost_log}" 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
log_success "Bifrost-http started (PID: ${BIFROST_PID})"
|
||||
}
|
||||
|
||||
# Extract latencies from a vegeta binary results file
|
||||
# Arguments: $1 = path to .bin file
|
||||
# Sets: EXTRACTED_MIN_NS, EXTRACTED_MEAN_NS, EXTRACTED_50_NS, etc.
|
||||
extract_latencies() {
|
||||
local bin_file=$1
|
||||
local json_report_file="${WORK_DIR}/vegeta-report.json"
|
||||
vegeta report -type=json < "${bin_file}" > "${json_report_file}"
|
||||
|
||||
if command -v jq &> /dev/null; then
|
||||
EXTRACTED_MIN_NS=$(jq '.latencies.min // 0' "${json_report_file}")
|
||||
EXTRACTED_MEAN_NS=$(jq '.latencies.mean // 0' "${json_report_file}")
|
||||
EXTRACTED_50_NS=$(jq '.latencies["50th"] // 0' "${json_report_file}")
|
||||
EXTRACTED_90_NS=$(jq '.latencies["90th"] // 0' "${json_report_file}")
|
||||
EXTRACTED_95_NS=$(jq '.latencies["95th"] // 0' "${json_report_file}")
|
||||
EXTRACTED_99_NS=$(jq '.latencies["99th"] // 0' "${json_report_file}")
|
||||
EXTRACTED_MAX_NS=$(jq '.latencies.max // 0' "${json_report_file}")
|
||||
EXTRACTED_SUCCESS=$(jq '.success // 0' "${json_report_file}")
|
||||
EXTRACTED_RATE=$(jq '.rate // 0' "${json_report_file}")
|
||||
EXTRACTED_THROUGHPUT=$(jq '.throughput // 0' "${json_report_file}")
|
||||
elif command -v python3 &> /dev/null; then
|
||||
EXTRACTED_MIN_NS=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('latencies', {}).get('min', 0))")
|
||||
EXTRACTED_MEAN_NS=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('latencies', {}).get('mean', 0))")
|
||||
EXTRACTED_50_NS=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('latencies', {}).get('50th', 0))")
|
||||
EXTRACTED_90_NS=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('latencies', {}).get('90th', 0))")
|
||||
EXTRACTED_95_NS=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('latencies', {}).get('95th', 0))")
|
||||
EXTRACTED_99_NS=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('latencies', {}).get('99th', 0))")
|
||||
EXTRACTED_MAX_NS=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('latencies', {}).get('max', 0))")
|
||||
EXTRACTED_SUCCESS=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('success', 0))")
|
||||
EXTRACTED_RATE=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('rate', 0))")
|
||||
EXTRACTED_THROUGHPUT=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('throughput', 0))")
|
||||
else
|
||||
log_error "Neither jq nor python3 found. Cannot parse JSON results."
|
||||
return 1
|
||||
fi
|
||||
|
||||
rm -f "${json_report_file}"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Phase 1: Overhead measurement (mocker at ${OVERHEAD_MOCKER_LATENCY_MS}ms)
|
||||
# ============================================================
|
||||
|
||||
# Calibration: Vegeta -> Mocker direct (with latency)
|
||||
# Measures: Vegeta HTTP client + localhost network round-trip + mocker response generation
|
||||
run_calibration() {
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ Calibration: Vegeta -> Mocker (${OVERHEAD_MOCKER_LATENCY_MS}ms, direct) ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
log_info "Measuring Vegeta + network baseline (mocker at ${OVERHEAD_MOCKER_LATENCY_MS}ms latency)"
|
||||
log_info "Duration: ${OVERHEAD_DURATION}s at ${RATE} RPS, ~$(( RATE * OVERHEAD_MOCKER_LATENCY_MS / 1000 )) concurrent"
|
||||
echo ""
|
||||
|
||||
local target_file="${WORK_DIR}/vegeta-target-calibration.json"
|
||||
local payload='{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Hello, how are you?"}]}'
|
||||
|
||||
cat > "${target_file}" << EOF
|
||||
{"method": "POST", "url": "http://localhost:${MOCKER_PORT}/v1/chat/completions", "header": {"Content-Type": ["application/json"], "Authorization": ["Bearer mocker-key"]}, "body": "$(echo -n "${payload}" | base64)"}
|
||||
EOF
|
||||
|
||||
vegeta attack \
|
||||
-format=json \
|
||||
-targets="${target_file}" \
|
||||
-rate="${RATE}" \
|
||||
-duration="${OVERHEAD_DURATION}s" \
|
||||
-timeout="$((OVERHEAD_MOCKER_LATENCY_MS / 1000 + 5))s" \
|
||||
-workers=$((RATE * OVERHEAD_MOCKER_LATENCY_MS / 1000)) \
|
||||
-max-workers="${MAX_WORKERS}" > "${WORK_DIR}/calibration.bin"
|
||||
|
||||
echo ""
|
||||
log_info "Calibration complete. Results:"
|
||||
vegeta report < "${WORK_DIR}/calibration.bin"
|
||||
|
||||
extract_latencies "${WORK_DIR}/calibration.bin"
|
||||
|
||||
log_info "Actual RPS: $(printf "%.0f" $EXTRACTED_RATE) (configured: ${RATE})"
|
||||
|
||||
CAL_MIN_NS=$EXTRACTED_MIN_NS
|
||||
CAL_MEAN_NS=$EXTRACTED_MEAN_NS
|
||||
CAL_50_NS=$EXTRACTED_50_NS
|
||||
CAL_90_NS=$EXTRACTED_90_NS
|
||||
CAL_95_NS=$EXTRACTED_95_NS
|
||||
CAL_99_NS=$EXTRACTED_99_NS
|
||||
CAL_MAX_NS=$EXTRACTED_MAX_NS
|
||||
|
||||
echo ""
|
||||
log_success "Calibration baseline (per bucket):"
|
||||
log_info " Min: $(echo "scale=2; $CAL_MIN_NS / 1000" | bc)µs"
|
||||
log_info " Mean: $(echo "scale=2; $CAL_MEAN_NS / 1000" | bc)µs"
|
||||
log_info " P50: $(echo "scale=2; $CAL_50_NS / 1000" | bc)µs"
|
||||
log_info " P90: $(echo "scale=2; $CAL_90_NS / 1000" | bc)µs"
|
||||
log_info " P95: $(echo "scale=2; $CAL_95_NS / 1000" | bc)µs"
|
||||
log_info " P99: $(echo "scale=2; $CAL_99_NS / 1000" | bc)µs"
|
||||
log_info " Max: $(echo "scale=2; $CAL_MAX_NS / 1000" | bc)µs"
|
||||
}
|
||||
|
||||
# Overhead test: Vegeta -> Bifrost -> Mocker (with latency)
|
||||
# Same duration/rate as calibration so percentile distributions are comparable
|
||||
run_overhead_test() {
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ Overhead Test: Vegeta -> Bifrost -> Mocker (${OVERHEAD_MOCKER_LATENCY_MS}ms) ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
log_info "Measuring Bifrost overhead (single instance, mocker at ${OVERHEAD_MOCKER_LATENCY_MS}ms latency)"
|
||||
log_info "Duration: ${OVERHEAD_DURATION}s at ${RATE} RPS, ~$(( RATE * OVERHEAD_MOCKER_LATENCY_MS / 1000 )) concurrent requests through Bifrost"
|
||||
log_info "Overhead consists of: vegetta overhead and mocker timeout jitter"
|
||||
echo ""
|
||||
|
||||
local target_file="${WORK_DIR}/vegeta-target.json"
|
||||
local payload='{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"Hello, how are you?"}]}'
|
||||
|
||||
cat > "${target_file}" << EOF
|
||||
{"method": "POST", "url": "http://localhost:${BIFROST_PORT}/v1/chat/completions", "header": {"Content-Type": ["application/json"]}, "body": "$(echo -n "${payload}" | base64)"}
|
||||
EOF
|
||||
|
||||
vegeta attack \
|
||||
-format=json \
|
||||
-targets="${target_file}" \
|
||||
-rate="${RATE}" \
|
||||
-duration="${OVERHEAD_DURATION}s" \
|
||||
-timeout="$((OVERHEAD_MOCKER_LATENCY_MS / 1000 + 5))s" \
|
||||
-workers=$((RATE * OVERHEAD_MOCKER_LATENCY_MS / 1000)) \
|
||||
-max-workers="${MAX_WORKERS}" > "${WORK_DIR}/attack.bin"
|
||||
|
||||
echo ""
|
||||
log_info "Overhead test complete. Results:"
|
||||
vegeta report < "${WORK_DIR}/attack.bin"
|
||||
|
||||
echo ""
|
||||
log_info "Latency histogram:"
|
||||
vegeta report -type=hist[0,100us,500us,1ms,5ms,10ms,50ms,100ms] < "${WORK_DIR}/attack.bin" || log_warn "Histogram generation failed"
|
||||
|
||||
# Extract and compute overhead
|
||||
extract_latencies "${WORK_DIR}/attack.bin"
|
||||
|
||||
log_info " Raw latencies (ns): min=$EXTRACTED_MIN_NS, mean=$EXTRACTED_MEAN_NS, p50=$EXTRACTED_50_NS, p99=$EXTRACTED_99_NS, max=$EXTRACTED_MAX_NS"
|
||||
log_info " Success rate: $EXTRACTED_SUCCESS"
|
||||
log_info " Actual RPS: $(printf "%.0f" $EXTRACTED_RATE) (configured: ${RATE})"
|
||||
|
||||
if [ -z "$EXTRACTED_MIN_NS" ] || [ "$EXTRACTED_MIN_NS" = "0" ] || [ "$EXTRACTED_MIN_NS" = "null" ]; then
|
||||
log_error "Failed to extract latency values from vegeta report"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Subtract calibration per bucket: overhead = through_bifrost - direct_to_mocker
|
||||
local us_min=$(printf "%.2f" $(echo "scale=4; ($EXTRACTED_MIN_NS - $CAL_MIN_NS) / 1000" | bc))
|
||||
local us_mean=$(printf "%.2f" $(echo "scale=4; ($EXTRACTED_MEAN_NS - $CAL_MEAN_NS) / 1000" | bc))
|
||||
local us_50=$(printf "%.2f" $(echo "scale=4; ($EXTRACTED_50_NS - $CAL_50_NS) / 1000" | bc))
|
||||
local us_90=$(printf "%.2f" $(echo "scale=4; ($EXTRACTED_90_NS - $CAL_90_NS) / 1000" | bc))
|
||||
local us_95=$(printf "%.2f" $(echo "scale=4; ($EXTRACTED_95_NS - $CAL_95_NS) / 1000" | bc))
|
||||
local us_99=$(printf "%.2f" $(echo "scale=4; ($EXTRACTED_99_NS - $CAL_99_NS) / 1000" | bc))
|
||||
local us_max=$(printf "%.2f" $(echo "scale=4; ($EXTRACTED_MAX_NS - $CAL_MAX_NS) / 1000" | bc))
|
||||
|
||||
local success_pct=$(printf "%.2f" $(echo "scale=4; $EXTRACTED_SUCCESS * 100" | bc))
|
||||
|
||||
echo ""
|
||||
log_success "Bifrost overhead (per bucket):"
|
||||
log_info " Min: ${us_min}µs"
|
||||
log_info " Mean: ${us_mean}µs"
|
||||
log_info " P50: ${us_50}µs"
|
||||
log_info " P90: ${us_90}µs"
|
||||
log_info " P95: ${us_95}µs"
|
||||
log_info " P99: ${us_99}µs"
|
||||
log_info " Max: ${us_max}µs"
|
||||
|
||||
local actual_rps=$(printf "%.0f" $EXTRACTED_RATE)
|
||||
|
||||
# Write results
|
||||
cat > "${RESULTS_FILE}" << EOF
|
||||
# Bifrost Load Test Results (single instance, ${actual_rps} RPS)
|
||||
|
||||
## Bifrost Processing Overhead
|
||||
|
||||
| Metric | Actual RPS | Duration | Concurrent | Success Rate | Min | Mean | P50 | P90 | P95 | P99 | Max |
|
||||
|--------|-----------|----------|------------|--------------|-----|------|-----|-----|-----|-----|-----|
|
||||
| Overhead | ${actual_rps} | ${OVERHEAD_DURATION}s | ~$((RATE * OVERHEAD_MOCKER_LATENCY_MS / 1000)) | ${success_pct}% | ${us_min}µs | ${us_mean}µs | ${us_50}µs | ${us_90}µs | ${us_95}µs | ${us_99}µs | ${us_max}µs |
|
||||
EOF
|
||||
|
||||
echo '{"overhead": {"configured_rate": '"${RATE}"', "actual_rate": '"${actual_rps}"', "duration": '"${OVERHEAD_DURATION}"', "concurrent": '$((RATE * OVERHEAD_MOCKER_LATENCY_MS / 1000))', "success_rate": '"${success_pct}"', "latency_us": {"min": '"${us_min}"', "mean": '"${us_mean}"', "p50": '"${us_50}"', "p90": '"${us_90}"', "p95": '"${us_95}"', "p99": '"${us_99}"', "max": '"${us_max}"'}}, "timestamp": "'"$(date -u +"%Y-%m-%dT%H:%M:%SZ")"'"}' > "${RESULTS_JSON}"
|
||||
|
||||
# Check tiered thresholds (skip Min/Max — single-point extremes are too noisy)
|
||||
local failed=0
|
||||
local labels=("Mean" "P50" "P90" "P95" "P99")
|
||||
local real_values=($EXTRACTED_MEAN_NS $EXTRACTED_50_NS $EXTRACTED_90_NS $EXTRACTED_95_NS $EXTRACTED_99_NS)
|
||||
local cal_values=($CAL_MEAN_NS $CAL_50_NS $CAL_90_NS $CAL_95_NS $CAL_99_NS)
|
||||
local thresholds=($MAX_OVERHEAD_MEAN_US $MAX_OVERHEAD_P50_US $MAX_OVERHEAD_P90_US $MAX_OVERHEAD_P95_US $MAX_OVERHEAD_P99_US)
|
||||
local extras=()
|
||||
|
||||
for i in "${!real_values[@]}"; do
|
||||
local overhead_us=$(( (real_values[i] - cal_values[i]) / 1000 ))
|
||||
if [ "$overhead_us" -gt "${thresholds[i]}" ]; then
|
||||
extras+=("${labels[i]}:${overhead_us}:${thresholds[i]}")
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$failed" -eq 1 ]; then
|
||||
echo ""
|
||||
log_error "FAILED: Bifrost overhead exceeded tiered thresholds"
|
||||
log_error "Overhead consists of: vegetta overhead and mocker timeout jitter. In real-world the P99 overhead will be approximately 100 microseconds."
|
||||
echo ""
|
||||
echo -e "${RED}| Bucket | Overhead (µs) | Threshold (µs) |${NC}"
|
||||
echo -e "${RED}|--------|---------------|----------------|${NC}"
|
||||
for entry in "${extras[@]}"; do
|
||||
IFS=: read -r bucket overhead threshold <<< "$entry"
|
||||
echo -e "${RED}| ${bucket} | ${overhead}µs | ${threshold}µs |${NC}"
|
||||
done
|
||||
echo ""
|
||||
stop_stats_monitor
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "All overhead buckets within tiered thresholds (mean<${MAX_OVERHEAD_MEAN_US}µs, p50<${MAX_OVERHEAD_P50_US}µs, p90<${MAX_OVERHEAD_P90_US}µs, p95<${MAX_OVERHEAD_P95_US}µs, p99<${MAX_OVERHEAD_P99_US}µs)"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Phase 2: Stress test (mocker at 10s latency)
|
||||
# ============================================================
|
||||
|
||||
# Arguments: $1 = label (e.g. "Stress #1", "Stress #2")
|
||||
run_stress_test() {
|
||||
local label="${1:-Stress}"
|
||||
local bin_file="${WORK_DIR}/stress.bin"
|
||||
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ ${label}: ${RATE} RPS with ${STRESS_MOCKER_LATENCY_MS}ms mocker latency ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
log_info "Testing single Bifrost instance under sustained concurrency"
|
||||
log_info "Duration: ${STRESS_DURATION}s at ${RATE} RPS (${STRESS_MOCKER_LATENCY_MS}ms mocker latency)"
|
||||
log_info "Expected concurrent requests: ~$(( RATE * STRESS_MOCKER_LATENCY_MS / 1000 )) (provider concurrency: 15,000, buffer: 20,000)"
|
||||
echo ""
|
||||
|
||||
local target_file="${WORK_DIR}/vegeta-target-stress.json"
|
||||
local payload='{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"Hello, how are you?"}]}'
|
||||
|
||||
cat > "${target_file}" << EOF
|
||||
{"method": "POST", "url": "http://localhost:${BIFROST_PORT}/v1/chat/completions", "header": {"Content-Type": ["application/json"]}, "body": "$(echo -n "${payload}" | base64)"}
|
||||
EOF
|
||||
|
||||
vegeta attack \
|
||||
-format=json \
|
||||
-targets="${target_file}" \
|
||||
-rate="${RATE}" \
|
||||
-duration="${STRESS_DURATION}s" \
|
||||
-timeout="30s" \
|
||||
-workers=$((RATE * STRESS_MOCKER_LATENCY_MS / 1000)) \
|
||||
-max-workers="${MAX_WORKERS}" > "${bin_file}"
|
||||
|
||||
echo ""
|
||||
log_info "${label} complete. Results:"
|
||||
vegeta report < "${bin_file}"
|
||||
|
||||
echo ""
|
||||
log_info "Latency histogram:"
|
||||
vegeta report -type=hist[0,1ms,5ms,10ms,50ms,100ms,500ms,1s,5s,10s,15s] < "${bin_file}" || log_warn "Histogram generation failed"
|
||||
|
||||
# Check success rate
|
||||
extract_latencies "${bin_file}"
|
||||
|
||||
local success_pct=$(printf "%.2f" $(echo "scale=4; $EXTRACTED_SUCCESS * 100" | bc))
|
||||
|
||||
log_info "Actual RPS: $(printf "%.0f" $EXTRACTED_RATE) (configured: ${RATE})"
|
||||
|
||||
local stress_actual_rps=$(printf "%.0f" $EXTRACTED_RATE)
|
||||
|
||||
# Append stress test results to results file
|
||||
cat >> "${RESULTS_FILE}" << EOF
|
||||
|
||||
## ${label} (${STRESS_MOCKER_LATENCY_MS}ms mocker latency)
|
||||
|
||||
| Metric | Actual RPS | Duration | Concurrent | Success Rate | Min | Mean | P50 | P90 | P95 | P99 | Max |
|
||||
|--------|-----------|----------|------------|--------------|-----|------|-----|-----|-----|-----|-----|
|
||||
| ${label} | ${stress_actual_rps} | ${STRESS_DURATION}s | ~$((RATE * STRESS_MOCKER_LATENCY_MS / 1000)) | ${success_pct}% | $(echo "scale=2; $EXTRACTED_MIN_NS / 1000000" | bc)ms | $(echo "scale=2; $EXTRACTED_MEAN_NS / 1000000" | bc)ms | $(echo "scale=2; $EXTRACTED_50_NS / 1000000" | bc)ms | $(echo "scale=2; $EXTRACTED_90_NS / 1000000" | bc)ms | $(echo "scale=2; $EXTRACTED_95_NS / 1000000" | bc)ms | $(echo "scale=2; $EXTRACTED_99_NS / 1000000" | bc)ms | $(echo "scale=2; $EXTRACTED_MAX_NS / 1000000" | bc)ms |
|
||||
EOF
|
||||
|
||||
if [ "$success_pct" != "100.00" ]; then
|
||||
echo ""
|
||||
log_error "FAILED: ${label} success rate is ${success_pct}% (expected 100%)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "${label} passed: ${success_pct}% success rate"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Finalize
|
||||
# ============================================================
|
||||
|
||||
finalize_results() {
|
||||
# Append process stats if available
|
||||
local has_overhead_stats=false
|
||||
local has_stress_stats=false
|
||||
|
||||
if [ -n "$OVERHEAD_STATS_CPU_PEAK" ]; then
|
||||
has_overhead_stats=true
|
||||
fi
|
||||
if [ -n "$STATS_CPU_PEAK" ]; then
|
||||
has_stress_stats=true
|
||||
fi
|
||||
|
||||
if [ "$has_overhead_stats" = true ] || [ "$has_stress_stats" = true ]; then
|
||||
cat >> "${RESULTS_FILE}" << 'EOF'
|
||||
|
||||
## Bifrost Process Stats (single instance)
|
||||
|
||||
| Phase | CPU Avg | CPU Peak | RSS Avg | RSS Peak |
|
||||
|-------|---------|----------|---------|----------|
|
||||
EOF
|
||||
|
||||
if [ "$has_overhead_stats" = true ]; then
|
||||
echo "| Overhead | ${OVERHEAD_STATS_CPU_AVG}% | ${OVERHEAD_STATS_CPU_PEAK}% | ${OVERHEAD_STATS_RSS_AVG}MB | ${OVERHEAD_STATS_RSS_PEAK}MB |" >> "${RESULTS_FILE}"
|
||||
fi
|
||||
if [ "$has_stress_stats" = true ]; then
|
||||
echo "| Stress | ${STATS_CPU_AVG}% | ${STATS_CPU_PEAK}% | ${STATS_RSS_AVG}MB | ${STATS_RSS_PEAK}MB |" >> "${RESULTS_FILE}"
|
||||
fi
|
||||
fi
|
||||
|
||||
cat >> "${RESULTS_FILE}" << EOF
|
||||
|
||||
## Method
|
||||
|
||||
- **Single instance**: All tests run against one bifrost-http process at ${RATE} RPS
|
||||
- **Overhead measurement**: Mocker at ${OVERHEAD_MOCKER_LATENCY_MS}ms latency, calibration (Vegeta->Mocker) subtracted from test (Vegeta->Bifrost->Mocker)
|
||||
- **Stress test**: Mocker at ${STRESS_MOCKER_LATENCY_MS}ms latency, verifies 100% success under sustained concurrency
|
||||
|
||||
## Notes
|
||||
|
||||
- Overhead values are in microseconds (µs), stress test values in milliseconds (ms)
|
||||
- Overhead ignores the mocker jitter, local network request queuing. In real-world the P99 overhead will be approximately 100 microseconds.
|
||||
- Tiered overhead thresholds: mean<${MAX_OVERHEAD_MEAN_US}µs, p50<${MAX_OVERHEAD_P50_US}µs, p90<${MAX_OVERHEAD_P90_US}µs, p95<${MAX_OVERHEAD_P95_US}µs, p99<${MAX_OVERHEAD_P99_US}µs
|
||||
- P50/P90/P95/P99 represent percentile latencies
|
||||
|
||||
---
|
||||
*Generated by Bifrost Load Test Script*
|
||||
EOF
|
||||
|
||||
# Update JSON with stress results and process stats
|
||||
local tmp_json=$(mktemp)
|
||||
if command -v jq &> /dev/null; then
|
||||
jq --arg sr "$(printf "%.2f" $(echo "scale=4; $EXTRACTED_SUCCESS * 100" | bc))" \
|
||||
--arg cpu_avg "${STATS_CPU_AVG:-0}" --arg cpu_peak "${STATS_CPU_PEAK:-0}" \
|
||||
--arg rss_avg "${STATS_RSS_AVG:-0}" --arg rss_peak "${STATS_RSS_PEAK:-0}" \
|
||||
--arg oh_cpu_avg "${OVERHEAD_STATS_CPU_AVG:-0}" --arg oh_cpu_peak "${OVERHEAD_STATS_CPU_PEAK:-0}" \
|
||||
--arg oh_rss_avg "${OVERHEAD_STATS_RSS_AVG:-0}" --arg oh_rss_peak "${OVERHEAD_STATS_RSS_PEAK:-0}" \
|
||||
'.stress = {"rate": '"${RATE}"', "duration": '"${STRESS_DURATION}"', "mocker_latency_ms": '"${STRESS_MOCKER_LATENCY_MS}"', "success_rate": ($sr | tonumber)} | .process_stats = {"overhead": {"cpu_avg_pct": ($oh_cpu_avg | tonumber), "cpu_peak_pct": ($oh_cpu_peak | tonumber), "rss_avg_mb": ($oh_rss_avg | tonumber), "rss_peak_mb": ($oh_rss_peak | tonumber)}, "stress": {"cpu_avg_pct": ($cpu_avg | tonumber), "cpu_peak_pct": ($cpu_peak | tonumber), "rss_avg_mb": ($rss_avg | tonumber), "rss_peak_mb": ($rss_peak | tonumber)}}' \
|
||||
"${RESULTS_JSON}" > "${tmp_json}"
|
||||
mv "${tmp_json}" "${RESULTS_JSON}"
|
||||
fi
|
||||
|
||||
log_success "Results saved to:"
|
||||
log_info " - Markdown: ${RESULTS_FILE}"
|
||||
log_info " - JSON: ${RESULTS_JSON}"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ Bifrost Load Test (single instance, ${RATE} RPS) ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
log_info "Configuration: single bifrost-http instance, ${RATE} RPS"
|
||||
log_info "Provider concurrency: 15,000 (buffer: 20,000)"
|
||||
log_info "Overhead thresholds: mean<${MAX_OVERHEAD_MEAN_US}µs, p50<${MAX_OVERHEAD_P50_US}µs, p90<${MAX_OVERHEAD_P90_US}µs, p95<${MAX_OVERHEAD_P95_US}µs, p99<${MAX_OVERHEAD_P99_US}µs"
|
||||
log_info "Phase 1: Overhead measurement — ${OVERHEAD_MOCKER_LATENCY_MS}ms mocker, ${OVERHEAD_DURATION}s, ~$(( RATE * OVERHEAD_MOCKER_LATENCY_MS / 1000 )) concurrent requests"
|
||||
log_info "Phase 2: Stress test — ${STRESS_MOCKER_LATENCY_MS}ms mocker, ${STRESS_DURATION}s, ~$(( RATE * STRESS_MOCKER_LATENCY_MS / 1000 )) concurrent requests"
|
||||
|
||||
check_dependencies
|
||||
install_vegeta
|
||||
build_bifrost_http
|
||||
setup_mocker
|
||||
build_mocker
|
||||
create_config
|
||||
cleanup_ports
|
||||
|
||||
# ── Phase 1: Overhead measurement with ${OVERHEAD_MOCKER_LATENCY_MS}ms mocker ──
|
||||
start_mocker ${OVERHEAD_MOCKER_LATENCY_MS}
|
||||
start_bifrost
|
||||
start_stats_monitor
|
||||
|
||||
run_calibration
|
||||
run_overhead_test
|
||||
|
||||
# ── Collect process stats from overhead phase ──
|
||||
stop_stats_monitor
|
||||
OVERHEAD_STATS_CPU_AVG="${STATS_CPU_AVG}"
|
||||
OVERHEAD_STATS_CPU_PEAK="${STATS_CPU_PEAK}"
|
||||
OVERHEAD_STATS_RSS_AVG="${STATS_RSS_AVG}"
|
||||
OVERHEAD_STATS_RSS_PEAK="${STATS_RSS_PEAK}"
|
||||
|
||||
# ── Phase 2: Stress test with high-latency mocker ──
|
||||
# Restart both mocker and bifrost to ensure a clean fasthttp connection pool.
|
||||
# Without restarting bifrost, stale TCP connections from the overhead phase
|
||||
# (which used a different mocker process) cause immediate 400s on POST requests
|
||||
# because fasthttp does not retry non-idempotent methods on broken connections.
|
||||
stop_mocker
|
||||
stop_bifrost
|
||||
start_mocker ${STRESS_MOCKER_LATENCY_MS}
|
||||
start_bifrost
|
||||
start_stats_monitor
|
||||
|
||||
run_stress_test "Stress #1"
|
||||
|
||||
echo ""
|
||||
log_info "Waiting 30s before second stress test (idle period)..."
|
||||
sleep 30
|
||||
|
||||
run_stress_test "Stress #2"
|
||||
|
||||
# ── Collect process stats from stress phase ──
|
||||
stop_stats_monitor
|
||||
|
||||
# ── Finalize ──
|
||||
finalize_results
|
||||
|
||||
cleanup_ports
|
||||
echo ""
|
||||
|
||||
# Print final summary
|
||||
echo "╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ FINAL RESULTS SUMMARY ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
cat "${RESULTS_FILE}"
|
||||
echo ""
|
||||
log_success "All tests passed!"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
250
.github/workflows/scripts/push-cli-mintlify-changelog.sh
vendored
Executable file
250
.github/workflows/scripts/push-cli-mintlify-changelog.sh
vendored
Executable file
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
VERSION=$1
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Usage: $0 <version>"
|
||||
echo "Example: $0 0.10.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION_WITH_PREFIX="cli-v$VERSION"
|
||||
|
||||
# Check if this page already exists in docs/changelogs/
|
||||
if [ -f "docs/changelogs/$VERSION_WITH_PREFIX.mdx" ]; then
|
||||
echo "✅ Changelog for $VERSION_WITH_PREFIX already exists"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Source changelog utilities
|
||||
source "$(dirname "$0")/changelog-utils.sh"
|
||||
|
||||
# Get current date
|
||||
CURRENT_DATE=$(date +"%Y-%m-%d")
|
||||
|
||||
# Get changelog content from cli/changelog.md
|
||||
CLI_CHANGELOG_PATH="cli/changelog.md"
|
||||
if [ ! -f "$CLI_CHANGELOG_PATH" ]; then
|
||||
echo "❌ CLI changelog not found at $CLI_CHANGELOG_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CHANGELOG_CONTENT=$(get_file_content "$CLI_CHANGELOG_PATH")
|
||||
if [ -z "$CHANGELOG_CONTENT" ]; then
|
||||
echo "❌ CLI changelog is empty"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Preparing changelog file
|
||||
CHANGELOG_BODY="---
|
||||
title: \"v$VERSION\"
|
||||
description: \"v$VERSION changelog - $CURRENT_DATE\"
|
||||
---
|
||||
|
||||
<Update label=\"Bifrost CLI\" description=\"v$VERSION\">
|
||||
$CHANGELOG_CONTENT
|
||||
|
||||
</Update>
|
||||
"
|
||||
|
||||
# Write to file
|
||||
mkdir -p docs/changelogs
|
||||
echo "$CHANGELOG_BODY" > "docs/changelogs/$VERSION_WITH_PREFIX.mdx"
|
||||
echo "✅ Created docs/changelogs/$VERSION_WITH_PREFIX.mdx"
|
||||
|
||||
# Clear the CLI changelog file after processing
|
||||
printf '' > "$CLI_CHANGELOG_PATH"
|
||||
echo "✅ Cleared $CLI_CHANGELOG_PATH"
|
||||
|
||||
# Update docs.json to include this new changelog route in the Bifrost CLI menu
|
||||
route="changelogs/$VERSION_WITH_PREFIX"
|
||||
if ! grep -q "\"$route\"" docs/docs.json; then
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const docs = JSON.parse(fs.readFileSync('docs/docs.json', 'utf8'));
|
||||
|
||||
// Semantic version comparison function
|
||||
// Extracts version from route/filename and compares in descending order (newest first)
|
||||
function compareVersionsDesc(a, b) {
|
||||
// Extract route string from string or object
|
||||
const routeA = typeof a === 'string' ? a : '';
|
||||
const routeB = typeof b === 'string' ? b : '';
|
||||
|
||||
// Extract version from route (e.g., 'changelogs/cli-v0.10.0' -> 'cli-v0.10.0')
|
||||
const versionA = routeA.split('/').pop() || '';
|
||||
const versionB = routeB.split('/').pop() || '';
|
||||
|
||||
// Remove 'cli-v' or 'v' prefix and split into parts
|
||||
const partsA = versionA.replace(/^(cli-)?v/, '').split(/[.-]/).map(p => {
|
||||
const num = parseInt(p, 10);
|
||||
return isNaN(num) ? p : num;
|
||||
});
|
||||
const partsB = versionB.replace(/^(cli-)?v/, '').split(/[.-]/).map(p => {
|
||||
const num = parseInt(p, 10);
|
||||
return isNaN(num) ? p : num;
|
||||
});
|
||||
|
||||
// Compare each part (major, minor, patch, pre-release, etc.)
|
||||
const maxLength = Math.max(partsA.length, partsB.length);
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
// Release vs prerelease: release is newer (no suffix > has suffix)
|
||||
if (partsA[i] === undefined && partsB[i] !== undefined) {
|
||||
return -1; // A (release) comes first in descending order
|
||||
}
|
||||
if (partsB[i] === undefined && partsA[i] !== undefined) {
|
||||
return 1; // B (release) comes first in descending order
|
||||
}
|
||||
|
||||
const partA = partsA[i];
|
||||
const partB = partsB[i];
|
||||
|
||||
// If both are numbers, compare numerically
|
||||
if (typeof partA === 'number' && typeof partB === 'number') {
|
||||
if (partA !== partB) {
|
||||
return partB - partA; // Descending order
|
||||
}
|
||||
} else {
|
||||
// Handle prerelease strings with numeric suffixes (e.g., 'prerelease10')
|
||||
const strA = String(partA);
|
||||
const strB = String(partB);
|
||||
const matchA = strA.match(/^([a-zA-Z]+)(\\d+)$/);
|
||||
const matchB = strB.match(/^([a-zA-Z]+)(\\d+)$/);
|
||||
|
||||
if (matchA && matchB && matchA[1] === matchB[1]) {
|
||||
// Same prefix, compare numbers numerically
|
||||
const numA = parseInt(matchA[2], 10);
|
||||
const numB = parseInt(matchB[2], 10);
|
||||
if (numA !== numB) {
|
||||
return numB - numA; // Descending order
|
||||
}
|
||||
} else if (strA !== strB) {
|
||||
return strB.localeCompare(strA); // Descending order
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // Equal
|
||||
}
|
||||
|
||||
// Sort a pages array by semver (descending)
|
||||
function sortPagesBySemver(pages) {
|
||||
return pages.slice().sort(compareVersionsDesc);
|
||||
}
|
||||
|
||||
// Get current month/year
|
||||
const releaseDate = new Date('$CURRENT_DATE');
|
||||
const currentDate = new Date();
|
||||
const releaseMonthYear = releaseDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
|
||||
const currentMonthYear = currentDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
|
||||
|
||||
// Find the Changelogs tab
|
||||
const changelogsTab = docs.navigation.tabs.find(tab => tab.tab === 'Changelogs');
|
||||
if (!changelogsTab) {
|
||||
console.error('Changelogs tab not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Find the Bifrost CLI menu item
|
||||
const cliMenuItem = changelogsTab.menu.find(item => item.item === 'Bifrost CLI');
|
||||
if (!cliMenuItem) {
|
||||
console.error('Bifrost CLI menu item not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get all top-level entries and existing groups
|
||||
const topLevelEntries = cliMenuItem.pages.filter(p => typeof p === 'string');
|
||||
const existingGroups = cliMenuItem.pages.filter(p => typeof p === 'object');
|
||||
|
||||
// Check if we need to group existing top-level entries
|
||||
if (topLevelEntries.length > 0) {
|
||||
// Get the month of the first top-level entry (they should all be from same month)
|
||||
const firstEntryPath = topLevelEntries[0].replace('changelogs/', '') + '.mdx';
|
||||
const firstEntryFile = 'docs/changelogs/' + firstEntryPath;
|
||||
|
||||
let topLevelMonth = null;
|
||||
try {
|
||||
const content = fs.readFileSync(firstEntryFile, 'utf8');
|
||||
const descMatch = content.match(/description:\\s*\"[^\"]*?(\\d{4}-\\d{2}-\\d{2})[^\"]*\"/);
|
||||
if (descMatch) {
|
||||
const entryDate = new Date(descMatch[1]);
|
||||
topLevelMonth = entryDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(\`Warning: Could not read entry file \${firstEntryFile}: \${e.message}\`);
|
||||
}
|
||||
|
||||
// Only group if the month has changed
|
||||
if (topLevelMonth && topLevelMonth !== releaseMonthYear) {
|
||||
console.log(\`📦 Month changed from \${topLevelMonth} to \${releaseMonthYear}\`);
|
||||
console.log(\`📦 Grouping \${topLevelEntries.length} top-level entries into \${topLevelMonth} group...\`);
|
||||
|
||||
// Create a group for all existing top-level entries
|
||||
const previousMonthGroup = {
|
||||
group: topLevelMonth,
|
||||
pages: sortPagesBySemver(topLevelEntries)
|
||||
};
|
||||
|
||||
// Add this group at the top of existing groups
|
||||
existingGroups.unshift(previousMonthGroup);
|
||||
console.log(\`✅ Created \${topLevelMonth} group with \${topLevelEntries.length} entries (sorted)\`);
|
||||
|
||||
// Clear top-level entries (they're now in the group)
|
||||
cliMenuItem.pages = existingGroups;
|
||||
} else {
|
||||
console.log(\`📋 Same month (\${releaseMonthYear}), keeping existing top-level entries\`);
|
||||
// Keep existing structure (top-level entries + groups)
|
||||
cliMenuItem.pages = [...topLevelEntries, ...existingGroups];
|
||||
}
|
||||
}
|
||||
|
||||
const newRoute = '$route';
|
||||
|
||||
// Add the new changelog at the top level
|
||||
cliMenuItem.pages.unshift(newRoute);
|
||||
console.log(\`✅ Added \${newRoute} to top level\`);
|
||||
|
||||
// Sort the top-level pages array by semver
|
||||
const topLevelPages = cliMenuItem.pages.filter(p => typeof p === 'string');
|
||||
const groupPages = cliMenuItem.pages.filter(p => typeof p === 'object');
|
||||
|
||||
if (topLevelPages.length > 0) {
|
||||
const sortedTopLevel = sortPagesBySemver(topLevelPages);
|
||||
cliMenuItem.pages = [...sortedTopLevel, ...groupPages];
|
||||
console.log(\`✅ Sorted \${topLevelPages.length} top-level pages by semver\`);
|
||||
}
|
||||
|
||||
// Sort each group's pages by semver
|
||||
for (const group of groupPages) {
|
||||
if (group.pages && Array.isArray(group.pages)) {
|
||||
group.pages = sortPagesBySemver(group.pages);
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync('docs/docs.json', JSON.stringify(docs, null, 2) + '\n');
|
||||
console.log('✅ Updated docs.json');
|
||||
"
|
||||
fi
|
||||
|
||||
# Pulling again before committing
|
||||
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
if [ "$CURRENT_BRANCH" = "HEAD" ]; then
|
||||
# In detached HEAD state (common in CI), use GITHUB_REF_NAME or default to main
|
||||
CURRENT_BRANCH="${GITHUB_REF_NAME:-main}"
|
||||
fi
|
||||
|
||||
echo "Pulling latest changes from origin/$CURRENT_BRANCH..."
|
||||
if ! git pull origin "$CURRENT_BRANCH"; then
|
||||
echo "❌ Error: git pull origin $CURRENT_BRANCH failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Commit and push changes
|
||||
git add "docs/changelogs/$VERSION_WITH_PREFIX.mdx"
|
||||
git add docs/docs.json
|
||||
git add "$CLI_CHANGELOG_PATH"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git commit -m "Adds CLI changelog for v$VERSION --skip-ci"
|
||||
git push origin "$CURRENT_BRANCH"
|
||||
|
||||
echo "✅ Pushed CLI changelog for v$VERSION"
|
||||
286
.github/workflows/scripts/push-mintlify-changelog.sh
vendored
Executable file
286
.github/workflows/scripts/push-mintlify-changelog.sh
vendored
Executable file
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
VERSION=$1
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Usage: $0 <version>"
|
||||
echo "Example: $0 1.2.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="v$VERSION"
|
||||
|
||||
# Check if this page already exists in docs/changelogs/
|
||||
if [ -f "docs/changelogs/$VERSION.mdx" ]; then
|
||||
echo "✅ Changelog for $VERSION already exists"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Source changelog utilities
|
||||
source "$(dirname "$0")/changelog-utils.sh"
|
||||
|
||||
# Get current date
|
||||
CURRENT_DATE=$(date +"%Y-%m-%d")
|
||||
|
||||
# Preparing changelog file
|
||||
CHANGELOG_BODY="---
|
||||
title: \"$VERSION\"
|
||||
description: \"$VERSION changelog - $CURRENT_DATE\"
|
||||
---
|
||||
<Tabs>
|
||||
<Tab title=\"NPX\">
|
||||
\`\`\`bash
|
||||
npx -y @maximhq/bifrost --transport-version $VERSION
|
||||
\`\`\`
|
||||
</Tab>
|
||||
<Tab title=\"Docker\">
|
||||
\`\`\`bash
|
||||
docker pull maximhq/bifrost:$VERSION
|
||||
docker run -p 8080:8080 maximhq/bifrost:$VERSION
|
||||
\`\`\`
|
||||
</Tab>
|
||||
</Tabs>
|
||||
"
|
||||
|
||||
# Array to track cleaned changelog files
|
||||
CLEANED_CHANGELOG_FILES=()
|
||||
|
||||
# Helper to append a section if changelog file exists and is non-empty
|
||||
append_section () {
|
||||
label=$1
|
||||
path=$2
|
||||
if [ -f "$path" ]; then
|
||||
# Get changelog content
|
||||
content=$(get_file_content "$path")
|
||||
# If changelog is empty, skip
|
||||
if [ -z "$content" ]; then
|
||||
echo "❌ Changelog is empty"
|
||||
return
|
||||
fi
|
||||
# Remove /changelog.md from the path and add /version
|
||||
version_file_path="${path%/changelog.md}/version"
|
||||
# Get version content
|
||||
version_body=$(get_file_content "$version_file_path")
|
||||
# Build the changelog section
|
||||
CHANGELOG_BODY+=$'\n'"<Update label=\"$label\" description=\"$version_body\">"$'\n'"$content"$'\n\n'"</Update>"
|
||||
# Clear the changelog file after processing
|
||||
printf '' > "$path"
|
||||
# Track this file for git commit
|
||||
CLEANED_CHANGELOG_FILES+=("$path")
|
||||
fi
|
||||
}
|
||||
|
||||
# HTTP changelog
|
||||
append_section "Bifrost(HTTP)" transports/changelog.md
|
||||
|
||||
# Core changelog
|
||||
append_section "Core" core/changelog.md
|
||||
|
||||
# Framework changelog
|
||||
append_section "Framework" framework/changelog.md
|
||||
|
||||
# Plugins changelogs
|
||||
for plugin in plugins/*; do
|
||||
name=$(basename "$plugin")
|
||||
append_section "$name" "$plugin/changelog.md"
|
||||
done
|
||||
|
||||
# Write to file
|
||||
mkdir -p docs/changelogs
|
||||
echo "$CHANGELOG_BODY" > docs/changelogs/$VERSION.mdx
|
||||
|
||||
# Update docs.json to include this new changelog route in the Changelogs tab pages array
|
||||
# Uses month-based grouping: current month at top level, older months in groups
|
||||
# Automatically reorganizes when month changes
|
||||
route="changelogs/$VERSION"
|
||||
if ! grep -q "\"$route\"" docs/docs.json; then
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const docs = JSON.parse(fs.readFileSync('docs/docs.json', 'utf8'));
|
||||
|
||||
// Semantic version comparison function
|
||||
// Extracts version from route/filename and compares in descending order (newest first)
|
||||
function compareVersionsDesc(a, b) {
|
||||
// Extract route string from string or object
|
||||
const routeA = typeof a === 'string' ? a : '';
|
||||
const routeB = typeof b === 'string' ? b : '';
|
||||
|
||||
// Extract version from route (e.g., 'changelogs/v1.3.34' -> 'v1.3.34')
|
||||
const versionA = routeA.split('/').pop() || '';
|
||||
const versionB = routeB.split('/').pop() || '';
|
||||
|
||||
// Remove 'v' prefix and split into parts
|
||||
const partsA = versionA.replace(/^v/, '').split(/[.-]/).map(p => {
|
||||
const num = parseInt(p, 10);
|
||||
return isNaN(num) ? p : num;
|
||||
});
|
||||
const partsB = versionB.replace(/^v/, '').split(/[.-]/).map(p => {
|
||||
const num = parseInt(p, 10);
|
||||
return isNaN(num) ? p : num;
|
||||
});
|
||||
|
||||
// Compare each part (major, minor, patch, pre-release, etc.)
|
||||
const maxLength = Math.max(partsA.length, partsB.length);
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
// Release vs prerelease: release is newer (no suffix > has suffix)
|
||||
if (partsA[i] === undefined && partsB[i] !== undefined) {
|
||||
return -1; // A (release) comes first in descending order
|
||||
}
|
||||
if (partsB[i] === undefined && partsA[i] !== undefined) {
|
||||
return 1; // B (release) comes first in descending order
|
||||
}
|
||||
|
||||
const partA = partsA[i];
|
||||
const partB = partsB[i];
|
||||
|
||||
// If both are numbers, compare numerically
|
||||
if (typeof partA === 'number' && typeof partB === 'number') {
|
||||
if (partA !== partB) {
|
||||
return partB - partA; // Descending order
|
||||
}
|
||||
} else {
|
||||
// Handle prerelease strings with numeric suffixes (e.g., 'prerelease10')
|
||||
const strA = String(partA);
|
||||
const strB = String(partB);
|
||||
const matchA = strA.match(/^([a-zA-Z]+)(\\d+)$/);
|
||||
const matchB = strB.match(/^([a-zA-Z]+)(\\d+)$/);
|
||||
|
||||
if (matchA && matchB && matchA[1] === matchB[1]) {
|
||||
// Same prefix, compare numbers numerically
|
||||
const numA = parseInt(matchA[2], 10);
|
||||
const numB = parseInt(matchB[2], 10);
|
||||
if (numA !== numB) {
|
||||
return numB - numA; // Descending order
|
||||
}
|
||||
} else if (strA !== strB) {
|
||||
return strB.localeCompare(strA); // Descending order
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // Equal
|
||||
}
|
||||
|
||||
// Sort a pages array by semver (descending)
|
||||
function sortPagesBySemver(pages) {
|
||||
return pages.slice().sort(compareVersionsDesc);
|
||||
}
|
||||
|
||||
// Get current month/year
|
||||
const releaseDate = new Date('$CURRENT_DATE');
|
||||
const currentDate = new Date();
|
||||
const releaseMonthYear = releaseDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
|
||||
const currentMonthYear = currentDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
|
||||
|
||||
// Find the Changelogs tab
|
||||
const changelogsTab = docs.navigation.tabs.find(tab => tab.tab === 'Changelogs');
|
||||
if (!changelogsTab) {
|
||||
console.error('Changelogs tab not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Find the Open Source menu item
|
||||
const openSourceItem = changelogsTab.menu?.find(item => item.item === 'Open Source');
|
||||
if (!openSourceItem) {
|
||||
console.error('Open Source menu item not found in Changelogs tab');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get all top-level entries and existing groups
|
||||
const topLevelEntries = openSourceItem.pages.filter(p => typeof p === 'string');
|
||||
const existingGroups = openSourceItem.pages.filter(p => typeof p === 'object');
|
||||
|
||||
// Check if we need to group existing top-level entries
|
||||
if (topLevelEntries.length > 0) {
|
||||
// Get the month of the first top-level entry (they should all be from same month)
|
||||
const firstEntryPath = topLevelEntries[0].replace('changelogs/', '') + '.mdx';
|
||||
const firstEntryFile = 'docs/changelogs/' + firstEntryPath;
|
||||
|
||||
let topLevelMonth = null;
|
||||
try {
|
||||
const content = fs.readFileSync(firstEntryFile, 'utf8');
|
||||
const descMatch = content.match(/description:\\s*\"[^\"]*?(\\d{4}-\\d{2}-\\d{2})[^\"]*\"/);
|
||||
if (descMatch) {
|
||||
const entryDate = new Date(descMatch[1]);
|
||||
topLevelMonth = entryDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(\`Warning: Could not read entry file \${firstEntryFile}: \${e.message}\`);
|
||||
}
|
||||
|
||||
// Only group if the month has changed
|
||||
if (topLevelMonth && topLevelMonth !== releaseMonthYear) {
|
||||
console.log(\`📦 Month changed from \${topLevelMonth} to \${releaseMonthYear}\`);
|
||||
console.log(\`📦 Grouping \${topLevelEntries.length} top-level entries into \${topLevelMonth} group...\`);
|
||||
|
||||
// Create a group for all existing top-level entries
|
||||
const previousMonthGroup = {
|
||||
group: topLevelMonth,
|
||||
pages: sortPagesBySemver(topLevelEntries)
|
||||
};
|
||||
|
||||
// Add this group at the top of existing groups
|
||||
existingGroups.unshift(previousMonthGroup);
|
||||
console.log(\`✅ Created \${topLevelMonth} group with \${topLevelEntries.length} entries (sorted)\`);
|
||||
|
||||
// Clear top-level entries (they're now in the group)
|
||||
openSourceItem.pages = existingGroups;
|
||||
} else {
|
||||
console.log(\`📋 Same month (\${releaseMonthYear}), keeping existing top-level entries\`);
|
||||
// Keep existing structure (top-level entries + groups)
|
||||
openSourceItem.pages = [...topLevelEntries, ...existingGroups];
|
||||
}
|
||||
}
|
||||
|
||||
const newRoute = '$route';
|
||||
|
||||
// Add the new changelog at the top level
|
||||
openSourceItem.pages.unshift(newRoute);
|
||||
console.log(\`✅ Added \${newRoute} to top level\`);
|
||||
|
||||
// Sort the top-level pages array by semver
|
||||
const topLevelPages = openSourceItem.pages.filter(p => typeof p === 'string');
|
||||
const groupPages = openSourceItem.pages.filter(p => typeof p === 'object');
|
||||
|
||||
if (topLevelPages.length > 0) {
|
||||
const sortedTopLevel = sortPagesBySemver(topLevelPages);
|
||||
openSourceItem.pages = [...sortedTopLevel, ...groupPages];
|
||||
console.log(\`✅ Sorted \${topLevelPages.length} top-level pages by semver\`);
|
||||
}
|
||||
|
||||
// Sort each group's pages by semver
|
||||
for (const group of groupPages) {
|
||||
if (group.pages && Array.isArray(group.pages)) {
|
||||
group.pages = sortPagesBySemver(group.pages);
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync('docs/docs.json', JSON.stringify(docs, null, 2) + '\n');
|
||||
console.log('✅ Updated docs.json');
|
||||
"
|
||||
fi
|
||||
|
||||
# Pulling again before committing
|
||||
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
if [ "$CURRENT_BRANCH" = "HEAD" ]; then
|
||||
# In detached HEAD state (common in CI), use GITHUB_REF_NAME or default to main
|
||||
CURRENT_BRANCH="${GITHUB_REF_NAME:-main}"
|
||||
fi
|
||||
|
||||
echo "Pulling latest changes from origin/$CURRENT_BRANCH..."
|
||||
if ! git pull origin "$CURRENT_BRANCH"; then
|
||||
echo "❌ Error: git pull origin $CURRENT_BRANCH failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Commit and push changes
|
||||
git add docs/changelogs/$VERSION.mdx
|
||||
git add docs/docs.json
|
||||
# Add all cleaned changelog files
|
||||
for file in "${CLEANED_CHANGELOG_FILES[@]}"; do
|
||||
git add "$file"
|
||||
done
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git commit -m "Adds changelog for $VERSION --skip-ci"
|
||||
git push origin "$CURRENT_BRANCH"
|
||||
110
.github/workflows/scripts/release-all-plugins.sh
vendored
Executable file
110
.github/workflows/scripts/release-all-plugins.sh
vendored
Executable file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Release all changed plugins sequentially
|
||||
# Usage: ./release-all-plugins.sh '["plugin1", "plugin2"]'
|
||||
|
||||
# Validate that an argument was provided
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "❌ Error: Missing required argument"
|
||||
echo "Usage: $0 '<JSON_ARRAY_OF_PLUGINS>'"
|
||||
echo "Example: $0 '[\"plugin1\", \"plugin2\"]'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CHANGED_PLUGINS_JSON="$1"
|
||||
|
||||
# Verify jq is available
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "❌ Error: jq is required but not installed"
|
||||
echo "Please install jq to parse JSON input"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate that the input is valid JSON
|
||||
if ! echo "$CHANGED_PLUGINS_JSON" | jq empty >/dev/null 2>&1; then
|
||||
echo "❌ Error: Invalid JSON provided"
|
||||
echo "Input: $CHANGED_PLUGINS_JSON"
|
||||
echo "Please provide a valid JSON array of plugin names"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
echo "🔌 Processing plugin releases..."
|
||||
echo "📋 Changed plugins JSON: $CHANGED_PLUGINS_JSON"
|
||||
|
||||
# No work early‐exit if array is empty
|
||||
if jq -e 'length==0' <<<"$CHANGED_PLUGINS_JSON" >/dev/null 2>&1; then
|
||||
echo "⏭️ No plugins to release"
|
||||
echo "success=true" >> "${GITHUB_OUTPUT:-/dev/null}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Convert JSON array to bash array using readarray to avoid word-splitting
|
||||
if ! readarray -t PLUGINS < <(echo "$CHANGED_PLUGINS_JSON" | jq -r '.[]' 2>/dev/null); then
|
||||
echo "❌ Error: Failed to parse plugin names from JSON"
|
||||
echo "Input: $CHANGED_PLUGINS_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify release-single-plugin.sh exists and is executable
|
||||
RELEASE_SCRIPT="./.github/workflows/scripts/release-single-plugin.sh"
|
||||
if [ ! -f "$RELEASE_SCRIPT" ]; then
|
||||
echo "❌ Error: Release script not found: $RELEASE_SCRIPT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$RELEASE_SCRIPT" ]; then
|
||||
echo "❌ Error: Release script is not executable: $RELEASE_SCRIPT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ${#PLUGINS[@]} -eq 0 ]; then
|
||||
echo "⏭️ No plugins to release"
|
||||
echo "success=true" >> "${GITHUB_OUTPUT:-/dev/null}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "🔄 Releasing ${#PLUGINS[@]} plugins:"
|
||||
for p in "${PLUGINS[@]}"; do
|
||||
echo " • $p"
|
||||
done
|
||||
|
||||
FAILED_PLUGINS=()
|
||||
SUCCESS_COUNT=0
|
||||
OVERALL_EXIT_CODE=0
|
||||
|
||||
# Release each plugin
|
||||
for plugin in "${PLUGINS[@]}"; do
|
||||
echo ""
|
||||
echo "🔌 Releasing plugin: $plugin"
|
||||
|
||||
# Capture the exit code of the plugin release
|
||||
if "$RELEASE_SCRIPT" "$plugin"; then
|
||||
PLUGIN_EXIT_CODE=$?
|
||||
echo "✅ Successfully released: $plugin"
|
||||
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||
else
|
||||
PLUGIN_EXIT_CODE=$?
|
||||
echo "❌ Failed to release plugin '$plugin' (exit code: $PLUGIN_EXIT_CODE)"
|
||||
FAILED_PLUGINS+=("$plugin")
|
||||
OVERALL_EXIT_CODE=1
|
||||
fi
|
||||
done
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "📋 Plugin Release Summary:"
|
||||
echo " ✅ Successful: $SUCCESS_COUNT/${#PLUGINS[@]}"
|
||||
echo " ❌ Failed: ${#FAILED_PLUGINS[@]}"
|
||||
|
||||
if [ ${#FAILED_PLUGINS[@]} -gt 0 ]; then
|
||||
echo " Failed plugins: ${FAILED_PLUGINS[*]}"
|
||||
echo "success=false" >> "${GITHUB_OUTPUT:-/dev/null}"
|
||||
echo "❌ Plugin release process completed with failures"
|
||||
exit $OVERALL_EXIT_CODE
|
||||
else
|
||||
echo " 🎉 All plugins released successfully!"
|
||||
echo "success=true" >> "${GITHUB_OUTPUT:-/dev/null}"
|
||||
echo "✅ All plugin releases completed successfully"
|
||||
fi
|
||||
199
.github/workflows/scripts/release-bifrost-http-finalize.sh
vendored
Executable file
199
.github/workflows/scripts/release-bifrost-http-finalize.sh
vendored
Executable file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Finalize bifrost-http release: changelog, tagging, GitHub release, R2 latest copy
|
||||
# Usage: ./release-bifrost-http-finalize.sh <version>
|
||||
|
||||
# Validate input argument
|
||||
if [ "${1:-}" = "" ]; then
|
||||
echo "Usage: $0 <version>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="$1"
|
||||
TAG_NAME="transports/v${VERSION}"
|
||||
|
||||
echo "🏷️ Finalizing bifrost-http v$VERSION release..."
|
||||
|
||||
# Get core and framework versions from version files
|
||||
CORE_VERSION="v$(tr -d '\n\r' < core/version)"
|
||||
FRAMEWORK_VERSION="v$(tr -d '\n\r' < framework/version)"
|
||||
|
||||
# Re-compute plugin versions from version files and transports/go.mod
|
||||
declare -A PLUGIN_VERSIONS
|
||||
PLUGINS_USED=()
|
||||
|
||||
for plugin_dir in plugins/*/; do
|
||||
if [ -d "$plugin_dir" ]; then
|
||||
plugin_name=$(basename "$plugin_dir")
|
||||
PLUGIN_VERSION="v$(tr -d '\n\r' < "${plugin_dir}version")"
|
||||
PLUGIN_VERSIONS["$plugin_name"]="$PLUGIN_VERSION"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check which plugins are actually used by the transport
|
||||
while IFS= read -r plugin_line; do
|
||||
plugin_name=$(echo "$plugin_line" | awk -F'/' '{print $NF}' | awk '{print $1}')
|
||||
plugin_version=$(echo "$plugin_line" | awk '{print $NF}')
|
||||
|
||||
# Use version file version if available, otherwise use go.mod version
|
||||
if [[ -n "${PLUGIN_VERSIONS[$plugin_name]:-}" ]]; then
|
||||
PLUGINS_USED+=("$plugin_name:${PLUGIN_VERSIONS[$plugin_name]}")
|
||||
else
|
||||
PLUGIN_VERSIONS["$plugin_name"]="$plugin_version"
|
||||
PLUGINS_USED+=("$plugin_name:$plugin_version")
|
||||
fi
|
||||
done < <(grep "github.com/maximhq/bifrost/plugins/" transports/go.mod)
|
||||
|
||||
echo "🔧 Versions:"
|
||||
echo " Core: $CORE_VERSION"
|
||||
echo " Framework: $FRAMEWORK_VERSION"
|
||||
echo " Plugins:"
|
||||
for plugin_name in "${!PLUGIN_VERSIONS[@]}"; do
|
||||
echo " - $plugin_name: ${PLUGIN_VERSIONS[$plugin_name]}"
|
||||
done
|
||||
|
||||
# Capturing changelog
|
||||
CHANGELOG_BODY=$(cat transports/changelog.md)
|
||||
# Skip comments from changelog
|
||||
CHANGELOG_BODY=$(echo "$CHANGELOG_BODY" | grep -v '^<!--' | grep -v '^-->')
|
||||
# If changelog is empty, return error
|
||||
if [ -z "$CHANGELOG_BODY" ]; then
|
||||
echo "❌ Changelog is empty"
|
||||
exit 1
|
||||
fi
|
||||
echo "📝 New changelog: $CHANGELOG_BODY"
|
||||
|
||||
# Finding previous tag
|
||||
echo "🔍 Finding previous tag..."
|
||||
PREV_TAG=$(git tag -l "transports/v*" | sort -V | tail -1)
|
||||
if [[ "$PREV_TAG" == "$TAG_NAME" ]]; then
|
||||
PREV_TAG=$(git tag -l "transports/v*" | sort -V | tail -2 | head -1)
|
||||
fi
|
||||
echo "🔍 Previous tag: $PREV_TAG"
|
||||
|
||||
# Get message of the tag
|
||||
echo "🔍 Getting previous tag message..."
|
||||
PREV_CHANGELOG=$(git tag -l --format='%(contents)' "$PREV_TAG")
|
||||
echo "📝 Previous changelog body: $PREV_CHANGELOG"
|
||||
|
||||
# Checking if tag message is the same as the changelog
|
||||
if [[ "$PREV_CHANGELOG" == "$CHANGELOG_BODY" ]]; then
|
||||
echo "❌ Changelog is the same as the previous changelog"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create and push tag
|
||||
echo "🏷️ Creating tag: $TAG_NAME"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git tag "$TAG_NAME" -m "Release transports v$VERSION" -m "$CHANGELOG_BODY"
|
||||
git push origin "$TAG_NAME"
|
||||
|
||||
# Create GitHub release
|
||||
TITLE="Bifrost HTTP v$VERSION"
|
||||
|
||||
# Mark prereleases when version contains a hyphen
|
||||
PRERELEASE_FLAG=""
|
||||
if [[ "$VERSION" == *-* ]]; then
|
||||
PRERELEASE_FLAG="--prerelease"
|
||||
fi
|
||||
|
||||
LATEST_FLAG=""
|
||||
if [[ "$VERSION" != *-* ]]; then
|
||||
LATEST_FLAG="--latest"
|
||||
fi
|
||||
|
||||
# Generate plugin version summary
|
||||
PLUGIN_UPDATES=""
|
||||
if [ ${#PLUGINS_USED[@]} -gt 0 ]; then
|
||||
PLUGIN_UPDATES="
|
||||
|
||||
### 🔌 Plugin Versions
|
||||
This release includes the following plugin versions:
|
||||
"
|
||||
for plugin_info in "${PLUGINS_USED[@]}"; do
|
||||
plugin_name="${plugin_info%%:*}"
|
||||
plugin_version="${plugin_info##*:}"
|
||||
PLUGIN_UPDATES="$PLUGIN_UPDATES- **$plugin_name**: \`$plugin_version\`
|
||||
"
|
||||
done
|
||||
else
|
||||
# Show all available plugin versions even if not directly used
|
||||
PLUGIN_UPDATES="
|
||||
|
||||
### 🔌 Available Plugin Versions
|
||||
The following plugin versions are compatible with this release:
|
||||
"
|
||||
for plugin_name in "${!PLUGIN_VERSIONS[@]}"; do
|
||||
plugin_version="${PLUGIN_VERSIONS[$plugin_name]}"
|
||||
PLUGIN_UPDATES="$PLUGIN_UPDATES- **$plugin_name**: \`$plugin_version\`
|
||||
"
|
||||
done
|
||||
fi
|
||||
|
||||
BODY="## Bifrost HTTP Transport Release v$VERSION
|
||||
|
||||
$CHANGELOG_BODY
|
||||
|
||||
### Installation
|
||||
|
||||
#### Docker
|
||||
\`\`\`bash
|
||||
docker run -p 8080:8080 maximhq/bifrost:v$VERSION
|
||||
\`\`\`
|
||||
|
||||
#### Binary Download
|
||||
\`\`\`bash
|
||||
npx @maximhq/bifrost --transport-version v$VERSION
|
||||
\`\`\`
|
||||
|
||||
### Docker Images
|
||||
- **\`maximhq/bifrost:v$VERSION\`** - This specific version
|
||||
- **\`maximhq/bifrost:latest\`** - Latest version (updated with this release)
|
||||
|
||||
---
|
||||
_This release was automatically created with dependencies: core \`$CORE_VERSION\`, framework \`$FRAMEWORK_VERSION\`. All plugins have been validated and updated._"
|
||||
|
||||
if [ -z "${GH_TOKEN:-}" ] && [ -z "${GITHUB_TOKEN:-}" ]; then
|
||||
echo "Error: GH_TOKEN or GITHUB_TOKEN is not set. Please export one to authenticate the GitHub CLI."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🎉 Creating GitHub release for $TITLE..."
|
||||
gh release create "$TAG_NAME" \
|
||||
--title "$TITLE" \
|
||||
--notes "$BODY" \
|
||||
${PRERELEASE_FLAG} ${LATEST_FLAG}
|
||||
|
||||
echo "✅ Bifrost HTTP released successfully"
|
||||
|
||||
# Copy versioned R2 path to latest/ for stable releases
|
||||
if [[ "$VERSION" != *-* ]]; then
|
||||
if [ -n "${R2_ENDPOINT:-}" ] && [ -n "${R2_BUCKET:-}" ]; then
|
||||
echo "📤 Copying versioned binaries to latest/ on R2..."
|
||||
R2_ENDPOINT="$(echo "$R2_ENDPOINT" | tr -d '[:space:]')"
|
||||
aws s3 sync "s3://$R2_BUCKET/bifrost/v$VERSION/" "s3://$R2_BUCKET/bifrost/latest/" \
|
||||
--endpoint-url "$R2_ENDPOINT" \
|
||||
--profile "${R2_AWS_PROFILE:-R2}" \
|
||||
--no-progress \
|
||||
--delete
|
||||
echo "✅ Latest binaries updated on R2"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Print summary
|
||||
echo ""
|
||||
echo "📋 Release Summary:"
|
||||
echo " 🏷️ Tag: $TAG_NAME"
|
||||
echo " 🔧 Core version: $CORE_VERSION"
|
||||
echo " 🔧 Framework version: $FRAMEWORK_VERSION"
|
||||
echo " 📦 Transport: Updated"
|
||||
if [ ${#PLUGINS_USED[@]} -gt 0 ]; then
|
||||
echo " 🔌 Plugins used: ${PLUGINS_USED[*]}"
|
||||
else
|
||||
echo " 🔌 Available plugins: $(printf "%s " "${!PLUGIN_VERSIONS[@]}")"
|
||||
fi
|
||||
echo " 🎉 GitHub release: Created"
|
||||
|
||||
echo "success=true" >> "$GITHUB_OUTPUT"
|
||||
157
.github/workflows/scripts/release-bifrost-http-prep.sh
vendored
Executable file
157
.github/workflows/scripts/release-bifrost-http-prep.sh
vendored
Executable file
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Prepare bifrost-http release: update dependencies, build UI, validate, commit/push
|
||||
# Usage: ./release-bifrost-http-prep.sh <version>
|
||||
|
||||
# Get the absolute path of the script directory
|
||||
# Use readlink if available (Linux), otherwise use cd/pwd (macOS compatible)
|
||||
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
else
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
fi
|
||||
|
||||
# Source Go utilities for exponential backoff
|
||||
source "$SCRIPT_DIR/go-utils.sh"
|
||||
|
||||
# Validate input argument
|
||||
if [ "${1:-}" = "" ]; then
|
||||
echo "Usage: $0 <version>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="$1"
|
||||
|
||||
echo "🚀 Preparing bifrost-http v$VERSION release..."
|
||||
|
||||
# Get core and framework versions from version files
|
||||
CORE_VERSION="v$(tr -d '\n\r' < core/version)"
|
||||
FRAMEWORK_VERSION="v$(tr -d '\n\r' < framework/version)"
|
||||
|
||||
echo "🔍 DEBUG: CORE_VERSION: $CORE_VERSION"
|
||||
echo "🔍 DEBUG: FRAMEWORK_VERSION: $FRAMEWORK_VERSION"
|
||||
|
||||
|
||||
# Get plugin versions from version files
|
||||
echo "🔌 Getting plugin versions from version files..."
|
||||
declare -A PLUGIN_VERSIONS
|
||||
|
||||
# Get versions for plugins that exist in the plugins/ directory
|
||||
for plugin_dir in plugins/*/; do
|
||||
if [ -d "$plugin_dir" ]; then
|
||||
plugin_name=$(basename "$plugin_dir")
|
||||
PLUGIN_VERSION="v$(tr -d '\n\r' < "${plugin_dir}version")"
|
||||
PLUGIN_VERSIONS["$plugin_name"]="$PLUGIN_VERSION"
|
||||
echo " 📦 $plugin_name: $PLUGIN_VERSION (from version file)"
|
||||
fi
|
||||
done
|
||||
|
||||
# Also check for any plugins already in transport go.mod that might not be in plugins/ directory
|
||||
cd transports
|
||||
echo "🔍 Checking for additional plugins in transport go.mod..."
|
||||
# Parse go.mod plugin lines and add missing ones
|
||||
while IFS= read -r plugin_line; do
|
||||
plugin_name=$(echo "$plugin_line" | awk -F'/' '{print $NF}' | awk '{print $1}')
|
||||
current_version=$(echo "$plugin_line" | awk '{print $NF}')
|
||||
|
||||
# Only add if we don't already have this plugin
|
||||
if [[ -z "${PLUGIN_VERSIONS[$plugin_name]:-}" ]]; then
|
||||
echo " 📦 $plugin_name: $current_version (from transport go.mod)"
|
||||
PLUGIN_VERSIONS["$plugin_name"]="$current_version"
|
||||
fi
|
||||
done < <(grep "github.com/maximhq/bifrost/plugins/" go.mod)
|
||||
cd ..
|
||||
|
||||
echo "🔧 Using versions:"
|
||||
echo " Core: $CORE_VERSION"
|
||||
echo " Framework: $FRAMEWORK_VERSION"
|
||||
echo " Plugins:"
|
||||
for plugin_name in "${!PLUGIN_VERSIONS[@]}"; do
|
||||
echo " - $plugin_name: ${PLUGIN_VERSIONS[$plugin_name]}"
|
||||
done
|
||||
|
||||
# Update transport dependencies to use plugin versions from version files
|
||||
echo "🔧 Using plugin versions from version files for transport..."
|
||||
|
||||
# Track which plugins are actually used by the transport
|
||||
cd transports
|
||||
|
||||
# Normalize the local go.mod directive up front so prior-release artifacts
|
||||
# (e.g. `go 1.26.2` written by earlier `go get` runs) don't trip GOTOOLCHAIN=local.
|
||||
go mod edit -go=1.26.1 -toolchain=none
|
||||
|
||||
for plugin_name in "${!PLUGIN_VERSIONS[@]}"; do
|
||||
plugin_version="${PLUGIN_VERSIONS[$plugin_name]}"
|
||||
|
||||
# Check if transport depends on this plugin
|
||||
if grep -q "github.com/maximhq/bifrost/plugins/$plugin_name" go.mod; then
|
||||
echo " 📦 Using $plugin_name plugin $plugin_version"
|
||||
# Textual require bump — skips loading the currently-declared version's go.mod
|
||||
go mod edit -require="github.com/maximhq/bifrost/plugins/$plugin_name@$plugin_version"
|
||||
fi
|
||||
done
|
||||
|
||||
# Also ensure core and framework are up to date
|
||||
|
||||
echo " 🔧 Updating core to $CORE_VERSION"
|
||||
go mod edit -require="github.com/maximhq/bifrost/core@$CORE_VERSION"
|
||||
|
||||
echo " 📦 Updating framework to $FRAMEWORK_VERSION"
|
||||
go mod edit -require="github.com/maximhq/bifrost/framework@$FRAMEWORK_VERSION"
|
||||
|
||||
# Re-normalize before tidy in case any edit reintroduced a toolchain line
|
||||
go mod edit -go=1.26.1 -toolchain=none
|
||||
go mod tidy
|
||||
|
||||
cd ..
|
||||
|
||||
# We need to build UI first before we can validate the transport build
|
||||
echo "🎨 Building UI..."
|
||||
make build-ui
|
||||
|
||||
# Building hello-world plugin
|
||||
echo "🔨 Building hello-world plugin..."
|
||||
cd examples/plugins/hello-world
|
||||
make build
|
||||
cd ../../..
|
||||
|
||||
# Validate transport build
|
||||
echo "🔨 Validating transport build..."
|
||||
cd transports
|
||||
go build ./...
|
||||
cd ..
|
||||
echo "✅ Transport build validation successful"
|
||||
|
||||
# Note: Migration tests run as a separate CI job (test-migrations) before this release job
|
||||
|
||||
# Commit and push changes if any
|
||||
# First, pull latest changes to avoid conflicts
|
||||
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
if [ "$CURRENT_BRANCH" = "HEAD" ]; then
|
||||
# In detached HEAD state (common in CI), use GITHUB_REF_NAME or default to main
|
||||
CURRENT_BRANCH="${GITHUB_REF_NAME:-main}"
|
||||
fi
|
||||
|
||||
echo "Pulling latest changes from origin/$CURRENT_BRANCH..."
|
||||
if ! git pull origin "$CURRENT_BRANCH"; then
|
||||
echo "❌ Error: git pull origin $CURRENT_BRANCH failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stage any changes made to transports/
|
||||
git add transports/
|
||||
|
||||
# Check if there are staged changes after pulling
|
||||
if ! git diff --cached --quiet; then
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
echo "🔧 Committing and pushing changes..."
|
||||
git commit -m "transports: update dependencies --skip-ci"
|
||||
git push -u origin HEAD
|
||||
else
|
||||
echo "ℹ️ No staged changes to commit"
|
||||
fi
|
||||
|
||||
echo "✅ Prep complete for bifrost-http v$VERSION"
|
||||
echo "success=true" >> "$GITHUB_OUTPUT"
|
||||
143
.github/workflows/scripts/release-cli.sh
vendored
Executable file
143
.github/workflows/scripts/release-cli.sh
vendored
Executable file
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Release bifrost CLI component
|
||||
# Usage: ./release-cli.sh <version>
|
||||
|
||||
# Get the absolute path of the script directory
|
||||
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
else
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
fi
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd -P)"
|
||||
|
||||
# Validate input argument
|
||||
if [ "${1:-}" = "" ]; then
|
||||
echo "Usage: $0 <version>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="$1"
|
||||
TAG_NAME="cli/v${VERSION}"
|
||||
|
||||
echo "🚀 Releasing bifrost CLI v$VERSION..."
|
||||
|
||||
# Validate CLI build
|
||||
echo "🔨 Validating CLI build..."
|
||||
COMMIT="${GITHUB_SHA:-$(git rev-parse HEAD 2>/dev/null || echo 'unknown')}"
|
||||
(cd "$REPO_ROOT/cli" && go build -ldflags "-X main.version=v${VERSION} -X main.commit=${COMMIT}" ./...)
|
||||
echo "✅ CLI build validation successful"
|
||||
|
||||
# Build CLI executables
|
||||
echo "🔨 Building executables..."
|
||||
bash "$SCRIPT_DIR/build-cli-executables.sh" "$VERSION"
|
||||
|
||||
# --- Preflight checks (no side effects) ---
|
||||
|
||||
# Capturing changelog
|
||||
CHANGELOG_BODY=$(cat "$REPO_ROOT/cli/changelog.md")
|
||||
# Skip comments from changelog
|
||||
CHANGELOG_BODY=$(printf '%s\n' "$CHANGELOG_BODY" | sed '/<!--/,/-->/d')
|
||||
# If changelog is empty, return error
|
||||
if [ -z "$CHANGELOG_BODY" ]; then
|
||||
echo "❌ Changelog is empty"
|
||||
exit 1
|
||||
fi
|
||||
echo "📝 New changelog: $CHANGELOG_BODY"
|
||||
|
||||
# Finding previous tag
|
||||
echo "🔍 Finding previous tag..."
|
||||
TAG_COUNT=$(git tag -l "cli/v*" | wc -l | tr -d ' ')
|
||||
if [[ "$TAG_COUNT" -eq 0 ]]; then
|
||||
PREV_TAG=""
|
||||
else
|
||||
PREV_TAG=$(git tag -l "cli/v*" | sort -V | tail -1)
|
||||
if [[ "$PREV_TAG" == "$TAG_NAME" ]]; then
|
||||
PREV_TAG=$(git tag -l "cli/v*" | sort -V | tail -2 | head -1)
|
||||
[[ "$PREV_TAG" == "$TAG_NAME" ]] && PREV_TAG=""
|
||||
fi
|
||||
fi
|
||||
echo "🔍 Previous tag: $PREV_TAG"
|
||||
|
||||
# Get message of the tag and compare changelogs
|
||||
if [[ -n "$PREV_TAG" ]]; then
|
||||
echo "🔍 Getting previous tag message..."
|
||||
PREV_CHANGELOG=$(git tag -l --format='%(contents:body)' "$PREV_TAG")
|
||||
echo "📝 Previous changelog body: $PREV_CHANGELOG"
|
||||
|
||||
# Checking if tag message is the same as the changelog
|
||||
if [[ "$PREV_CHANGELOG" == "$CHANGELOG_BODY" ]]; then
|
||||
echo "❌ Changelog is the same as the previous changelog"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ No previous CLI tag found. Skipping changelog comparison."
|
||||
fi
|
||||
|
||||
# Verify GitHub token before any publish steps
|
||||
if [ -z "${GH_TOKEN:-}" ] && [ -z "${GITHUB_TOKEN:-}" ]; then
|
||||
echo "Error: GH_TOKEN or GITHUB_TOKEN is not set. Please export one to authenticate the GitHub CLI."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Publish steps (all checks passed) ---
|
||||
|
||||
# Configure and upload to R2
|
||||
echo "📤 Uploading binaries..."
|
||||
bash "$SCRIPT_DIR/configure-r2.sh"
|
||||
bash "$SCRIPT_DIR/upload-cli-to-r2.sh" "$TAG_NAME"
|
||||
|
||||
# Create and push tag
|
||||
if git rev-parse -q --verify "refs/tags/$TAG_NAME" >/dev/null; then
|
||||
echo "ℹ️ Tag $TAG_NAME already exists. Reusing it."
|
||||
else
|
||||
echo "🏷️ Creating tag: $TAG_NAME"
|
||||
git tag "$TAG_NAME" -m "Release CLI v$VERSION" -m "$CHANGELOG_BODY"
|
||||
git push origin "$TAG_NAME"
|
||||
fi
|
||||
|
||||
# Create GitHub release
|
||||
TITLE="Bifrost CLI v$VERSION"
|
||||
|
||||
# Mark prereleases when version contains a hyphen
|
||||
PRERELEASE_FLAG=""
|
||||
if [[ "$VERSION" == *-* ]]; then
|
||||
PRERELEASE_FLAG="--prerelease"
|
||||
fi
|
||||
|
||||
BODY="## Bifrost CLI Release v$VERSION
|
||||
|
||||
$CHANGELOG_BODY
|
||||
|
||||
### Installation
|
||||
|
||||
#### Binary Download
|
||||
\`\`\`bash
|
||||
npx @maximhq/bifrost --cli-version v$VERSION
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
_This release was automatically created._"
|
||||
|
||||
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
|
||||
echo "ℹ️ GitHub release $TAG_NAME already exists. Skipping."
|
||||
else
|
||||
echo "🎉 Creating GitHub release for $TITLE..."
|
||||
gh release create "$TAG_NAME" \
|
||||
--title "$TITLE" \
|
||||
--notes "$BODY" \
|
||||
${PRERELEASE_FLAG}
|
||||
fi
|
||||
|
||||
echo "✅ Bifrost CLI released successfully"
|
||||
|
||||
# Print summary
|
||||
echo ""
|
||||
echo "📋 Release Summary:"
|
||||
echo " 🏷️ Tag: $TAG_NAME"
|
||||
echo " 🎉 GitHub release: Created"
|
||||
|
||||
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
|
||||
echo "success=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
103
.github/workflows/scripts/release-core.sh
vendored
Executable file
103
.github/workflows/scripts/release-core.sh
vendored
Executable file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Release core component
|
||||
# Usage: ./release-core.sh <version>
|
||||
|
||||
if [[ "${1:-}" == "" ]]; then
|
||||
echo "Usage: $0 <version>"
|
||||
echo "Example: $0 1.2.0"
|
||||
exit 1
|
||||
fi
|
||||
VERSION="$1"
|
||||
|
||||
TAG_NAME="core/v${VERSION}"
|
||||
|
||||
echo "🔧 Releasing core v$VERSION..."
|
||||
|
||||
# Validate core build
|
||||
echo "🔨 Validating core build..."
|
||||
cd core
|
||||
|
||||
if [[ ! -f version ]]; then
|
||||
echo "❌ Missing core/version file"
|
||||
exit 1
|
||||
fi
|
||||
FILE_VERSION="$(cat version | tr -d '[:space:]')"
|
||||
if [[ "$FILE_VERSION" != "$VERSION" ]]; then
|
||||
echo "❌ Version mismatch: arg=$VERSION, core/version=$FILE_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ..
|
||||
|
||||
# Capturing changelog
|
||||
CHANGELOG_BODY=$(cat core/changelog.md)
|
||||
# Skip comments from changelog
|
||||
CHANGELOG_BODY=$(echo "$CHANGELOG_BODY" | grep -v '^<!--' | grep -v '^-->')
|
||||
# If changelog is empty, return error
|
||||
if [ -z "$CHANGELOG_BODY" ]; then
|
||||
echo "❌ Changelog is empty"
|
||||
exit 1
|
||||
fi
|
||||
echo "📝 New changelog: $CHANGELOG_BODY"
|
||||
|
||||
# Finding previous tag
|
||||
echo "🔍 Finding previous tag..."
|
||||
PREV_TAG=$(git tag -l "core/v*" | sort -V | tail -1)
|
||||
if [[ "$PREV_TAG" == "$TAG_NAME" ]]; then
|
||||
PREV_TAG=$(git tag -l "core/v*" | sort -V | tail -2 | head -1)
|
||||
fi
|
||||
echo "🔍 Previous tag: $PREV_TAG"
|
||||
|
||||
# Get message of the tag
|
||||
echo "🔍 Getting previous tag message..."
|
||||
PREV_CHANGELOG=$(git tag -l --format='%(contents)' "$PREV_TAG")
|
||||
echo "📝 Previous changelog body: $PREV_CHANGELOG"
|
||||
|
||||
# Checking if tag message is the same as the changelog
|
||||
if [[ "$PREV_CHANGELOG" == "$CHANGELOG_BODY" ]]; then
|
||||
echo "❌ Changelog is the same as the previous changelog"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create and push tag
|
||||
echo "🏷️ Creating tag: $TAG_NAME"
|
||||
git tag "$TAG_NAME" -m "Release core v$VERSION" -m "$CHANGELOG_BODY"
|
||||
git push origin "$TAG_NAME"
|
||||
|
||||
# Create GitHub release
|
||||
TITLE="Core v$VERSION"
|
||||
|
||||
# Mark prereleases when version contains a hyphen
|
||||
PRERELEASE_FLAG=""
|
||||
if [[ "$VERSION" == *-* ]]; then
|
||||
PRERELEASE_FLAG="--prerelease"
|
||||
fi
|
||||
|
||||
LATEST_FLAG=""
|
||||
if [[ "$VERSION" != *-* ]]; then
|
||||
LATEST_FLAG="--latest"
|
||||
fi
|
||||
|
||||
BODY="## Core Release v$VERSION
|
||||
|
||||
$CHANGELOG_BODY
|
||||
|
||||
### Installation
|
||||
|
||||
\`\`\`bash
|
||||
go get github.com/maximhq/bifrost/core@v$VERSION
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
_This release was automatically created from version file: \`core/version\`_"
|
||||
|
||||
echo "🎉 Creating GitHub release for $TITLE..."
|
||||
gh release create "$TAG_NAME" \
|
||||
--title "$TITLE" \
|
||||
--notes "$BODY" \
|
||||
${PRERELEASE_FLAG} ${LATEST_FLAG}
|
||||
|
||||
echo "✅ Core released successfully"
|
||||
echo "success=true" >> "$GITHUB_OUTPUT"
|
||||
183
.github/workflows/scripts/release-framework.sh
vendored
Executable file
183
.github/workflows/scripts/release-framework.sh
vendored
Executable file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Release framework component
|
||||
# Usage: ./release-framework.sh <version>
|
||||
|
||||
# Source Go utilities for exponential backoff
|
||||
source "$(dirname "$0")/go-utils.sh"
|
||||
|
||||
# Making sure version is provided
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $0 <version>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION_RAW="$1"
|
||||
# Ensure leading 'v' for module/tag semver
|
||||
if [[ "$VERSION_RAW" == v* ]]; then
|
||||
VERSION="$VERSION_RAW"
|
||||
else
|
||||
VERSION="v$VERSION_RAW"
|
||||
fi
|
||||
|
||||
TAG_NAME="framework/${VERSION}"
|
||||
|
||||
echo "📦 Releasing framework $VERSION..."
|
||||
|
||||
# Ensure we have the latest version
|
||||
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
if [ "$CURRENT_BRANCH" = "HEAD" ]; then
|
||||
# In detached HEAD state (common in CI), use GITHUB_REF_NAME or default to main
|
||||
CURRENT_BRANCH="${GITHUB_REF_NAME:-main}"
|
||||
fi
|
||||
|
||||
echo "Pulling latest changes from origin/$CURRENT_BRANCH..."
|
||||
if ! git pull origin "$CURRENT_BRANCH"; then
|
||||
echo "❌ Error: git pull origin $CURRENT_BRANCH failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for merge conflicts or unexpected working-tree changes
|
||||
if ! git diff --quiet; then
|
||||
echo "❌ Error: Unstaged changes detected after pull (possible merge conflict)"
|
||||
git status --short
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! git diff --cached --quiet; then
|
||||
echo "❌ Error: Staged changes detected after pull (unexpected state)"
|
||||
git status --short
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fetching all tags
|
||||
git fetch --tags >/dev/null 2>&1 || true
|
||||
|
||||
# Get core version from version file
|
||||
CORE_VERSION="v$(tr -d '\n\r' < core/version)"
|
||||
|
||||
|
||||
# Before starting the test, we need to update hello-word plugin core dependencies
|
||||
echo "🔧 Updating hello-word plugin core dependencies..."
|
||||
cd examples/plugins/hello-world
|
||||
go_get_with_backoff "github.com/maximhq/bifrost/core@$CORE_VERSION"
|
||||
go mod tidy
|
||||
git add go.mod go.sum
|
||||
cd ../../..
|
||||
|
||||
echo "🔧 Using core version: $CORE_VERSION"
|
||||
|
||||
# Update framework dependencies
|
||||
echo "🔧 Updating framework dependencies..."
|
||||
cd framework
|
||||
go_get_with_backoff "github.com/maximhq/bifrost/core@$CORE_VERSION"
|
||||
go mod tidy
|
||||
git add go.mod go.sum
|
||||
|
||||
# Check if there are any changes to commit
|
||||
git add go.mod go.sum
|
||||
|
||||
|
||||
# Validate framework build
|
||||
echo "🔨 Validating framework build..."
|
||||
go build ./...
|
||||
cd ..
|
||||
echo "✅ Framework build validation successful"
|
||||
|
||||
# Check if there are any changes to commit
|
||||
if ! git diff --cached --quiet; then
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git commit -m "framework: bump core to $CORE_VERSION --skip-ci"
|
||||
# Push the bump so go.mod/go.sum changes are recorded on the branch
|
||||
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
if [ "$CURRENT_BRANCH" = "HEAD" ]; then
|
||||
# In detached HEAD state (common in CI), use GITHUB_REF_NAME or default to main
|
||||
CURRENT_BRANCH="${GITHUB_REF_NAME:-main}"
|
||||
fi
|
||||
git push origin "$CURRENT_BRANCH"
|
||||
echo "🔧 Pushed framework bump to $CURRENT_BRANCH"
|
||||
else
|
||||
echo "No dependency changes detected; skipping commit."
|
||||
fi
|
||||
|
||||
# Capturing changelog
|
||||
CHANGELOG_BODY=$(cat framework/changelog.md)
|
||||
# Skip comments from changelog
|
||||
CHANGELOG_BODY=$(echo "$CHANGELOG_BODY" | grep -v '^<!--' | grep -v '^-->')
|
||||
# If changelog is empty, return error
|
||||
if [ -z "$CHANGELOG_BODY" ]; then
|
||||
echo "❌ Changelog is empty"
|
||||
exit 1
|
||||
fi
|
||||
echo "📝 New changelog: $CHANGELOG_BODY"
|
||||
|
||||
# Finding previous tag
|
||||
echo "🔍 Finding previous tag..."
|
||||
PREV_TAG=$(git tag -l "framework/v*" | sort -V | tail -1)
|
||||
if [[ "$PREV_TAG" == "$TAG_NAME" ]]; then
|
||||
PREV_TAG=$(git tag -l "framework/v*" | sort -V | tail -2 | head -1)
|
||||
fi
|
||||
echo "🔍 Previous tag: $PREV_TAG"
|
||||
|
||||
# Get message of the tag
|
||||
echo "🔍 Getting previous tag message..."
|
||||
PREV_CHANGELOG=$(git tag -l --format='%(contents)' "$PREV_TAG")
|
||||
echo "📝 Previous changelog body: $PREV_CHANGELOG"
|
||||
|
||||
# Checking if tag message is the same as the changelog
|
||||
if [[ "$PREV_CHANGELOG" == "$CHANGELOG_BODY" ]]; then
|
||||
echo "❌ Changelog is the same as the previous changelog"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create and push tag
|
||||
echo "🏷️ Creating tag: $TAG_NAME"
|
||||
if git rev-parse --verify "$TAG_NAME" >/dev/null 2>&1; then
|
||||
echo "Tag $TAG_NAME already exists; skipping tag creation."
|
||||
else
|
||||
git tag "$TAG_NAME" -m "Release framework $VERSION" -m "$CHANGELOG_BODY"
|
||||
git push origin "$TAG_NAME"
|
||||
fi
|
||||
|
||||
# Create GitHub release
|
||||
TITLE="Framework $VERSION"
|
||||
|
||||
# Mark prereleases when version contains a hyphen
|
||||
PRERELEASE_FLAG=""
|
||||
if [[ "$VERSION" == *-* ]]; then
|
||||
PRERELEASE_FLAG="--prerelease"
|
||||
fi
|
||||
|
||||
LATEST_FLAG=""
|
||||
if [[ "$VERSION" != *-* ]]; then
|
||||
LATEST_FLAG="--latest"
|
||||
fi
|
||||
|
||||
BODY="## Framework Release $VERSION
|
||||
|
||||
$CHANGELOG_BODY
|
||||
|
||||
### Installation
|
||||
|
||||
\`\`\`bash
|
||||
go get github.com/maximhq/bifrost/framework@$VERSION
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
_This release was automatically created and uses core version: \`$CORE_VERSION\`_"
|
||||
|
||||
echo "🎉 Creating GitHub release for $TITLE..."
|
||||
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
|
||||
echo "ℹ️ Release $TAG_NAME already exists. Skipping creation."
|
||||
else
|
||||
gh release create "$TAG_NAME" \
|
||||
--title "$TITLE" \
|
||||
--notes "$BODY" \
|
||||
${PRERELEASE_FLAG} ${LATEST_FLAG}
|
||||
|
||||
fi
|
||||
|
||||
echo "✅ Framework released successfully"
|
||||
echo "success=true" >> "$GITHUB_OUTPUT"
|
||||
183
.github/workflows/scripts/release-single-plugin.sh
vendored
Executable file
183
.github/workflows/scripts/release-single-plugin.sh
vendored
Executable file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Release a single plugin
|
||||
# Usage: ./release-single-plugin.sh <plugin-name> [core-version] [framework-version]
|
||||
|
||||
# Source Go utilities for exponential backoff
|
||||
source "$(dirname "$0")/go-utils.sh"
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: $0 <plugin-name> [core-version] [framework-version]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PLUGIN_NAME="$1"
|
||||
|
||||
# Get core version from parameter or version file
|
||||
if [ -n "${2:-}" ]; then
|
||||
CORE_VERSION="$2"
|
||||
else
|
||||
CORE_VERSION="v$(tr -d '\n\r' < core/version)"
|
||||
fi
|
||||
|
||||
# Get framework version from parameter or version file
|
||||
if [ -n "${3:-}" ]; then
|
||||
FRAMEWORK_VERSION="$3"
|
||||
else
|
||||
FRAMEWORK_VERSION="v$(tr -d '\n\r' < framework/version)"
|
||||
fi
|
||||
|
||||
# Ensure we have the latest version
|
||||
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
if [ "$CURRENT_BRANCH" = "HEAD" ]; then
|
||||
# In detached HEAD state (common in CI), use GITHUB_REF_NAME or default to main
|
||||
CURRENT_BRANCH="${GITHUB_REF_NAME:-main}"
|
||||
fi
|
||||
|
||||
echo "Pulling latest changes from origin/$CURRENT_BRANCH..."
|
||||
if ! git pull origin "$CURRENT_BRANCH"; then
|
||||
echo "❌ Error: git pull origin $CURRENT_BRANCH failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🔌 Releasing plugin: $PLUGIN_NAME"
|
||||
echo "🔧 Core version: $CORE_VERSION"
|
||||
echo "🔧 Framework version: $FRAMEWORK_VERSION"
|
||||
|
||||
PLUGIN_DIR="plugins/$PLUGIN_NAME"
|
||||
VERSION_FILE="$PLUGIN_DIR/version"
|
||||
|
||||
if [ ! -f "$VERSION_FILE" ]; then
|
||||
echo "❌ Version file not found: $VERSION_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PLUGIN_VERSION=$(tr -d '\n\r' < "$VERSION_FILE")
|
||||
TAG_NAME="plugins/${PLUGIN_NAME}/v${PLUGIN_VERSION}"
|
||||
|
||||
echo "📦 Plugin version: $PLUGIN_VERSION"
|
||||
echo "🏷️ Tag name: $TAG_NAME"
|
||||
|
||||
|
||||
# Update plugin dependencies
|
||||
echo "🔧 Updating plugin dependencies..."
|
||||
cd "$PLUGIN_DIR"
|
||||
|
||||
# Update core dependency
|
||||
if [ -f "go.mod" ]; then
|
||||
go_get_with_backoff "github.com/maximhq/bifrost/core@${CORE_VERSION}"
|
||||
go_get_with_backoff "github.com/maximhq/bifrost/framework@${FRAMEWORK_VERSION}"
|
||||
go mod tidy
|
||||
git add go.mod go.sum || true
|
||||
|
||||
# Validate build
|
||||
echo "🔨 Validating plugin build..."
|
||||
go build ./...
|
||||
echo "✅ Plugin $PLUGIN_NAME build validation successful"
|
||||
else
|
||||
echo "ℹ️ No go.mod found, skipping Go dependency update"
|
||||
fi
|
||||
|
||||
cd ../..
|
||||
|
||||
# Commit and push changes if any
|
||||
if ! git diff --cached --quiet; then
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
echo "🔧 Committing and pushing changes..."
|
||||
git commit -m "plugins/${PLUGIN_NAME}: bump core to $CORE_VERSION and framework to $FRAMEWORK_VERSION --skip-ci"
|
||||
git push -u origin HEAD
|
||||
else
|
||||
echo "ℹ️ No staged changes to commit"
|
||||
fi
|
||||
|
||||
# Capturing changelog
|
||||
CHANGELOG_BODY=$(cat $PLUGIN_DIR/changelog.md)
|
||||
# Skip comments from changelog
|
||||
CHANGELOG_BODY=$(echo "$CHANGELOG_BODY" | grep -v '^<!--' | grep -v '^-->' || true)
|
||||
# If changelog is empty, return error
|
||||
if [ -z "$CHANGELOG_BODY" ]; then
|
||||
echo "❌ Changelog is empty"
|
||||
exit 1
|
||||
fi
|
||||
echo "📝 New changelog: $CHANGELOG_BODY"
|
||||
|
||||
# Finding previous tag
|
||||
echo "🔍 Finding previous tag..."
|
||||
PREV_TAG=$(git tag -l "plugins/${PLUGIN_NAME}/v*" | sort -V | tail -1)
|
||||
if [[ "$PREV_TAG" == "$TAG_NAME" ]]; then
|
||||
PREV_TAG=$(git tag -l "plugins/${PLUGIN_NAME}/v*" | sort -V | tail -2 | head -1)
|
||||
fi
|
||||
|
||||
# Only validate changelog changes if there's a previous tag
|
||||
if [ -n "$PREV_TAG" ]; then
|
||||
echo "🔍 Previous tag: $PREV_TAG"
|
||||
|
||||
# Get message of the tag
|
||||
echo "🔍 Getting previous tag message..."
|
||||
PREV_CHANGELOG=$(git tag -l --format='%(contents)' "$PREV_TAG")
|
||||
echo "📝 Previous changelog body: $PREV_CHANGELOG"
|
||||
|
||||
# Checking if tag message is the same as the changelog
|
||||
if [[ "$PREV_CHANGELOG" == "$CHANGELOG_BODY" ]]; then
|
||||
echo "❌ Changelog is the same as the previous changelog"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ No previous tag found - this is the first release"
|
||||
fi
|
||||
|
||||
|
||||
# Create and push tag
|
||||
echo "🏷️ Creating tag: $TAG_NAME"
|
||||
|
||||
if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then
|
||||
echo "ℹ️ Tag already exists: $TAG_NAME (skipping creation)"
|
||||
else
|
||||
git tag "$TAG_NAME" -m "Release plugin $PLUGIN_NAME v$PLUGIN_VERSION" -m "$CHANGELOG_BODY"
|
||||
git push origin "$TAG_NAME"
|
||||
fi
|
||||
|
||||
# Create GitHub release
|
||||
TITLE="Plugin $PLUGIN_NAME v$PLUGIN_VERSION"
|
||||
|
||||
# Mark prereleases when version contains a hyphen
|
||||
PRERELEASE_FLAG=""
|
||||
if [[ "$PLUGIN_VERSION" == *-* ]]; then
|
||||
PRERELEASE_FLAG="--prerelease"
|
||||
fi
|
||||
|
||||
# Mark as latest if not a prerelease
|
||||
LATEST_FLAG=""
|
||||
if [[ "$PLUGIN_VERSION" != *-* ]]; then
|
||||
LATEST_FLAG="--latest"
|
||||
fi
|
||||
|
||||
|
||||
BODY="## Plugin Release: $PLUGIN_NAME v$PLUGIN_VERSION
|
||||
|
||||
$CHANGELOG_BODY
|
||||
|
||||
### Installation
|
||||
|
||||
\`\`\`bash
|
||||
# Update your go.mod to use the new plugin version
|
||||
go get github.com/maximhq/bifrost/plugins/$PLUGIN_NAME@v$PLUGIN_VERSION
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
_This release was automatically created from version file: \`plugins/$PLUGIN_NAME/version\`_"
|
||||
|
||||
echo "🎉 Creating GitHub release for $TITLE..."
|
||||
|
||||
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
|
||||
echo "ℹ️ Release $TAG_NAME already exists. Skipping creation."
|
||||
else
|
||||
gh release create "$TAG_NAME" \
|
||||
--title "$TITLE" \
|
||||
--notes "$BODY" \
|
||||
${PRERELEASE_FLAG} ${LATEST_FLAG}
|
||||
fi
|
||||
|
||||
echo "✅ Plugin $PLUGIN_NAME released successfully"
|
||||
echo "success=true" >> "${GITHUB_OUTPUT:-/dev/null}"
|
||||
77
.github/workflows/scripts/revert-latest.sh
vendored
Executable file
77
.github/workflows/scripts/revert-latest.sh
vendored
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Overwrite latest with a specific version from R2
|
||||
# Usage: ./revert-latest.sh <version>
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 <version> (e.g., v1.2.3)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="$1"
|
||||
# Ensure version starts with 'v'
|
||||
if [[ ! "$VERSION" =~ ^v ]]; then
|
||||
VERSION="v${VERSION}"
|
||||
fi
|
||||
|
||||
# Validate required environment variables
|
||||
: "${R2_ENDPOINT:?R2_ENDPOINT env var is required}"
|
||||
: "${R2_BUCKET:?R2_BUCKET env var is required}"
|
||||
|
||||
# Clean endpoint URL
|
||||
R2_ENDPOINT="$(echo "$R2_ENDPOINT" | tr -d '[:space:]')"
|
||||
|
||||
echo "🔄 Reverting latest to version: $VERSION"
|
||||
|
||||
# Function to sync with retry logic
|
||||
sync_with_retry() {
|
||||
local source_path="$1"
|
||||
local dest_path="$2"
|
||||
local max_retries=3
|
||||
|
||||
for attempt in $(seq 1 $max_retries); do
|
||||
echo "🔄 Attempt $attempt/$max_retries: Syncing $source_path to $dest_path"
|
||||
|
||||
if aws s3 sync "$source_path" "$dest_path" \
|
||||
--endpoint-url "$R2_ENDPOINT" \
|
||||
--profile "${R2_AWS_PROFILE:-R2}" \
|
||||
--no-progress \
|
||||
--delete; then
|
||||
echo "✅ Sync successful from $source_path to $dest_path"
|
||||
return 0
|
||||
else
|
||||
echo "⚠️ Attempt $attempt failed"
|
||||
if [ $attempt -lt $max_retries ]; then
|
||||
delay=$((2 ** attempt))
|
||||
echo "🕐 Waiting ${delay}s before retry..."
|
||||
sleep $delay
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "❌ All $max_retries attempts failed for syncing to $dest_path"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if the version exists in R2
|
||||
echo "🔍 Checking if version $VERSION exists..."
|
||||
if ! aws s3 ls "s3://$R2_BUCKET/bifrost/$VERSION/" \
|
||||
--endpoint-url "$R2_ENDPOINT" \
|
||||
--profile "${R2_AWS_PROFILE:-R2}" >/dev/null 2>&1; then
|
||||
echo "❌ Version $VERSION not found in R2 bucket"
|
||||
echo "Available versions:"
|
||||
aws s3 ls "s3://$R2_BUCKET/bifrost/" \
|
||||
--endpoint-url "$R2_ENDPOINT" \
|
||||
--profile "${R2_AWS_PROFILE:-R2}" | grep "PRE v" | awk '{print $2}' | sed 's/\///g' || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Version $VERSION found in R2"
|
||||
|
||||
# Sync the specific version to latest
|
||||
if ! sync_with_retry "s3://$R2_BUCKET/bifrost/$VERSION/" "s3://$R2_BUCKET/bifrost/latest/"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🎉 Successfully reverted latest to version $VERSION"
|
||||
199
.github/workflows/scripts/run-governance-e2e-tests.sh
vendored
Executable file
199
.github/workflows/scripts/run-governance-e2e-tests.sh
vendored
Executable file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Run Governance E2E Tests
|
||||
# This script builds Bifrost, starts it with the governance test config,
|
||||
# runs the governance tests, and cleans up.
|
||||
#
|
||||
# Usage: ./run-governance-e2e-tests.sh
|
||||
|
||||
echo "🛡️ Starting Governance E2E Tests..."
|
||||
|
||||
# Get the root directory of the repo
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
BIFROST_PORT=8080
|
||||
BIFROST_HOST="localhost"
|
||||
BIFROST_URL="http://${BIFROST_HOST}:${BIFROST_PORT}"
|
||||
APP_DIR="tests/governance"
|
||||
BIFROST_BINARY="tmp/bifrost-http"
|
||||
BIFROST_PID_FILE="/tmp/bifrost-governance-test.pid"
|
||||
BIFROST_LOG_FILE="/tmp/bifrost-governance-test.log"
|
||||
MAX_STARTUP_WAIT=30 # seconds
|
||||
|
||||
# Cleanup function to ensure Bifrost is stopped
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
echo ""
|
||||
echo -e "${YELLOW}🧹 Cleaning up...${NC}"
|
||||
|
||||
# Stop Bifrost if running
|
||||
if [ -f "$BIFROST_PID_FILE" ]; then
|
||||
BIFROST_PID=$(cat "$BIFROST_PID_FILE")
|
||||
if ps -p "$BIFROST_PID" > /dev/null 2>&1; then
|
||||
echo -e "${CYAN}Stopping Bifrost (PID: $BIFROST_PID)...${NC}"
|
||||
kill "$BIFROST_PID" 2>/dev/null || true
|
||||
sleep 2
|
||||
# Force kill if still running
|
||||
if ps -p "$BIFROST_PID" > /dev/null 2>&1; then
|
||||
echo -e "${YELLOW}Force killing Bifrost...${NC}"
|
||||
kill -9 "$BIFROST_PID" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
rm -f "$BIFROST_PID_FILE"
|
||||
fi
|
||||
|
||||
# Clean up log file
|
||||
if [ -f "$BIFROST_LOG_FILE" ]; then
|
||||
echo -e "${CYAN}Bifrost logs saved to: $BIFROST_LOG_FILE${NC}"
|
||||
fi
|
||||
|
||||
# Clean up test database
|
||||
if [ -f "data/governance-test.db" ]; then
|
||||
echo -e "${CYAN}Cleaning up test database...${NC}"
|
||||
rm -f "data/governance-test.db"
|
||||
fi
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
echo -e "${GREEN}✅ Cleanup complete${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Cleanup complete (tests failed)${NC}"
|
||||
fi
|
||||
|
||||
exit $exit_code
|
||||
}
|
||||
|
||||
# Set up trap to cleanup on exit
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
# Step 1: Validate prerequisites
|
||||
echo -e "${CYAN}📋 Step 1: Validating prerequisites...${NC}"
|
||||
|
||||
if [ ! -d "$APP_DIR" ]; then
|
||||
echo -e "${RED}❌ App directory not found: $APP_DIR${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$APP_DIR/config.json" ]; then
|
||||
echo -e "${RED}❌ Config file not found: $APP_DIR/config.json${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check required environment variables (OPENAI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY)
|
||||
if [ -z "${OPENAI_API_KEY:-}" ] || [ -z "${ANTHROPIC_API_KEY:-}" ] || [ -z "${OPENROUTER_API_KEY:-}" ]; then
|
||||
echo -e "${RED}❌ Required environment variables are not set${NC}"
|
||||
echo -e "${YELLOW}Set them with: export OPENAI_API_KEY='sk-...'${NC}"
|
||||
echo -e "${YELLOW}Set them with: export ANTHROPIC_API_KEY='sk-...'${NC}"
|
||||
echo -e "${YELLOW}Set them with: export OPENROUTER_API_KEY='sk-...'${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✅ Prerequisites validated${NC}"
|
||||
|
||||
# Step 2: Build Bifrost
|
||||
echo ""
|
||||
echo -e "${CYAN}📦 Step 2: Building Bifrost...${NC}"
|
||||
|
||||
# Use make to build with LOCAL=1 to use the workspace (go.work)
|
||||
# This ensures we test the local governance plugin code, not the published version
|
||||
if ! make build LOCAL=1; then
|
||||
echo -e "${RED}❌ Failed to build Bifrost${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$BIFROST_BINARY" ]; then
|
||||
echo -e "${RED}❌ Bifrost binary not found at: $BIFROST_BINARY${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ Bifrost built successfully${NC}"
|
||||
|
||||
# Step 3: Start Bifrost in background
|
||||
echo ""
|
||||
echo -e "${CYAN}🚀 Step 3: Starting Bifrost server...${NC}"
|
||||
|
||||
# Ensure data directory exists for SQLite database
|
||||
mkdir -p data
|
||||
|
||||
# Start Bifrost in background
|
||||
echo -e "${YELLOW}Starting Bifrost on ${BIFROST_URL}...${NC}"
|
||||
"$BIFROST_BINARY" -app-dir "$APP_DIR" -port "$BIFROST_PORT" -host "$BIFROST_HOST" > "$BIFROST_LOG_FILE" 2>&1 &
|
||||
BIFROST_PID=$!
|
||||
echo "$BIFROST_PID" > "$BIFROST_PID_FILE"
|
||||
|
||||
echo -e "${CYAN}Bifrost started with PID: $BIFROST_PID${NC}"
|
||||
|
||||
# Step 4: Wait for Bifrost to be ready
|
||||
echo ""
|
||||
echo -e "${CYAN}⏳ Step 4: Waiting for Bifrost to be ready...${NC}"
|
||||
|
||||
WAIT_COUNT=0
|
||||
until curl -sf "${BIFROST_URL}/health" > /dev/null 2>&1; do
|
||||
if [ $WAIT_COUNT -ge $MAX_STARTUP_WAIT ]; then
|
||||
echo -e "${RED}❌ Bifrost failed to start within ${MAX_STARTUP_WAIT} seconds${NC}"
|
||||
echo -e "${YELLOW}Last 50 lines of Bifrost logs:${NC}"
|
||||
tail -n 50 "$BIFROST_LOG_FILE" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if process is still running
|
||||
if ! ps -p "$BIFROST_PID" > /dev/null 2>&1; then
|
||||
echo -e "${RED}❌ Bifrost process died${NC}"
|
||||
echo -e "${YELLOW}Bifrost logs:${NC}"
|
||||
cat "$BIFROST_LOG_FILE" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WAIT_COUNT=$((WAIT_COUNT + 1))
|
||||
echo -e "${YELLOW}Waiting for Bifrost... ($WAIT_COUNT/${MAX_STARTUP_WAIT})${NC}"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo -e "${GREEN}✅ Bifrost is ready and responding${NC}"
|
||||
|
||||
# Step 5: Run governance tests
|
||||
echo ""
|
||||
echo -e "${CYAN}🧪 Step 5: Running governance tests...${NC}"
|
||||
|
||||
cd tests/governance
|
||||
|
||||
# Run tests with go test (disable workspace to avoid module conflicts)
|
||||
echo -e "${YELLOW}Running go test in tests/governance...${NC}"
|
||||
|
||||
# Run tests with verbose output and timeout
|
||||
# Use GOWORK=off to disable the workspace file and test the module independently
|
||||
# Use -count=1 to disable test cache
|
||||
GOWORK=off go test -v -timeout 10m -count=1 ./...
|
||||
TEST_EXIT_CODE=$?
|
||||
if [ $TEST_EXIT_CODE -ne 0 ]; then
|
||||
echo -e "${RED}❌ Governance tests failed (exit code: $TEST_EXIT_CODE)${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✅ All governance tests passed${NC}"
|
||||
fi
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# Step 6: Report results
|
||||
echo ""
|
||||
if [ $TEST_EXIT_CODE -eq 0 ]; then
|
||||
echo -e "${GREEN}═══════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN}✅ Governance E2E Tests PASSED${NC}"
|
||||
echo -e "${GREEN}═══════════════════════════════════════════════════════${NC}"
|
||||
else
|
||||
echo -e "${RED}═══════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${RED}❌ Governance E2E Tests FAILED${NC}"
|
||||
echo -e "${RED}═══════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Check logs at: $BIFROST_LOG_FILE${NC}"
|
||||
fi
|
||||
|
||||
exit $TEST_EXIT_CODE
|
||||
299
.github/workflows/scripts/run-integration-tests.sh
vendored
Executable file
299
.github/workflows/scripts/run-integration-tests.sh
vendored
Executable file
@@ -0,0 +1,299 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Run integration tests with Bifrost binary and PostgreSQL
|
||||
# Usage: ./run-integration-tests.sh <bifrost-binary-path> [port]
|
||||
|
||||
# Get the absolute path of the script directory
|
||||
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
else
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
fi
|
||||
|
||||
# Repository root (3 levels up from .github/workflows/scripts)
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd -P)"
|
||||
|
||||
# Parse arguments
|
||||
if [ "${1:-}" = "" ]; then
|
||||
echo "Usage: $0 <bifrost-binary-path> [port]" >&2
|
||||
echo "" >&2
|
||||
echo "Arguments:" >&2
|
||||
echo " bifrost-binary-path Path to the bifrost-http binary" >&2
|
||||
echo " port Port to run Bifrost on (default: 8080)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BIFROST_BINARY="$1"
|
||||
PORT="${2:-8080}"
|
||||
|
||||
# PostgreSQL configuration (from environment or defaults)
|
||||
POSTGRES_HOST="${POSTGRES_HOST:-localhost}"
|
||||
POSTGRES_PORT="${POSTGRES_PORT:-5432}"
|
||||
POSTGRES_USER="${POSTGRES_USER:-bifrost}"
|
||||
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-bifrost_password}"
|
||||
POSTGRES_DB="${POSTGRES_DB:-bifrost}"
|
||||
POSTGRES_SSLMODE="${POSTGRES_SSLMODE:-disable}"
|
||||
|
||||
# Validate binary exists and is executable
|
||||
if [ ! -f "$BIFROST_BINARY" ]; then
|
||||
echo "❌ Error: Bifrost binary not found: $BIFROST_BINARY" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$BIFROST_BINARY" ]; then
|
||||
echo "❌ Error: Bifrost binary is not executable: $BIFROST_BINARY" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🧪 Running Bifrost Integration Tests"
|
||||
echo " Binary: $BIFROST_BINARY"
|
||||
echo " Port: $PORT"
|
||||
|
||||
# Create temp directory for merged config
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
MERGED_CONFIG="$TEMP_DIR/config.json"
|
||||
echo "📁 Using temp directory: $TEMP_DIR"
|
||||
|
||||
# Cleanup function
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
echo ""
|
||||
echo "🧹 Cleaning up..."
|
||||
|
||||
# Kill Bifrost server if running
|
||||
if [ -n "${BIFROST_PID:-}" ]; then
|
||||
echo " Stopping Bifrost server (PID: $BIFROST_PID)..."
|
||||
kill "$BIFROST_PID" 2>/dev/null || true
|
||||
wait "$BIFROST_PID" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Remove temp directory
|
||||
if [ -d "$TEMP_DIR" ]; then
|
||||
echo " Removing temp directory..."
|
||||
rm -rf "$TEMP_DIR"
|
||||
fi
|
||||
|
||||
exit $exit_code
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Create merged config
|
||||
echo "📝 Creating merged config with PostgreSQL..."
|
||||
|
||||
# Base config from tests/integrations
|
||||
BASE_CONFIG="$REPO_ROOT/tests/integrations/config.json"
|
||||
|
||||
if [ ! -f "$BASE_CONFIG" ]; then
|
||||
echo "❌ Error: Base config not found: $BASE_CONFIG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use jq to merge configs if available, otherwise use Python
|
||||
#
|
||||
# NOTE: The following config merge INTENTIONALLY OVERWRITES any existing
|
||||
# config_store and logs_store keys from the base config. This is required
|
||||
# because:
|
||||
# 1. Integration tests MUST use the local PostgreSQL instance to validate
|
||||
# database-related functionality (config persistence, logging, etc.)
|
||||
# 2. The base config (tests/integrations/config.json) typically has these
|
||||
# stores disabled; we need to fully replace them with enabled PostgreSQL
|
||||
# config pointing to the test container.
|
||||
# 3. Deep-merging is NOT desired here - we need a complete, known-good
|
||||
# PostgreSQL configuration regardless of what the base config contains.
|
||||
#
|
||||
# Edge cases handled:
|
||||
# - Base config has no store keys: jq/Python adds them (no issue)
|
||||
# - Base config has stores disabled: fully replaced with enabled PostgreSQL
|
||||
# - Base config has different store type (e.g., sqlite): fully replaced
|
||||
# - Base config has partial PostgreSQL config: fully replaced to ensure
|
||||
# correct credentials for the test container
|
||||
#
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
# jq '. + {...}' performs shallow merge at top level, fully replacing
|
||||
# config_store and logs_store keys (intentional - see note above)
|
||||
jq --arg host "$POSTGRES_HOST" \
|
||||
--arg port "$POSTGRES_PORT" \
|
||||
--arg user "$POSTGRES_USER" \
|
||||
--arg pass "$POSTGRES_PASSWORD" \
|
||||
--arg db "$POSTGRES_DB" \
|
||||
--arg ssl "$POSTGRES_SSLMODE" \
|
||||
'. + {
|
||||
"config_store": {
|
||||
"enabled": true,
|
||||
"type": "postgres",
|
||||
"config": {
|
||||
"host": $host,
|
||||
"port": $port,
|
||||
"user": $user,
|
||||
"password": $pass,
|
||||
"db_name": $db,
|
||||
"ssl_mode": $ssl
|
||||
}
|
||||
},
|
||||
"logs_store": {
|
||||
"enabled": true,
|
||||
"type": "postgres",
|
||||
"config": {
|
||||
"host": $host,
|
||||
"port": $port,
|
||||
"user": $user,
|
||||
"password": $pass,
|
||||
"db_name": $db,
|
||||
"ssl_mode": $ssl
|
||||
}
|
||||
}
|
||||
}' "$BASE_CONFIG" > "$MERGED_CONFIG"
|
||||
else
|
||||
# Fallback to Python if jq is not available
|
||||
# Same intentional overwrite behavior as jq path (see note above)
|
||||
python3 - "$BASE_CONFIG" "$MERGED_CONFIG" << 'EOF'
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
|
||||
base_path = sys.argv[1]
|
||||
merged_path = sys.argv[2]
|
||||
|
||||
with open(base_path, "r") as f:
|
||||
config = json.load(f)
|
||||
|
||||
postgres_config = {
|
||||
"host": os.environ.get("POSTGRES_HOST", "localhost"),
|
||||
"port": os.environ.get("POSTGRES_PORT", "5432"),
|
||||
"user": os.environ.get("POSTGRES_USER", "bifrost"),
|
||||
"password": os.environ.get("POSTGRES_PASSWORD", "bifrost_password"),
|
||||
"db_name": os.environ.get("POSTGRES_DB", "bifrost"),
|
||||
"ssl_mode": os.environ.get("POSTGRES_SSLMODE", "disable")
|
||||
}
|
||||
|
||||
# Intentionally overwrite any existing store config to force PostgreSQL
|
||||
# for integration tests (see detailed note in bash section above)
|
||||
config["config_store"] = {
|
||||
"enabled": True,
|
||||
"type": "postgres",
|
||||
"config": postgres_config
|
||||
}
|
||||
|
||||
config["logs_store"] = {
|
||||
"enabled": True,
|
||||
"type": "postgres",
|
||||
"config": postgres_config.copy()
|
||||
}
|
||||
|
||||
with open(merged_path, "w") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo " ✅ Merged config created at: $MERGED_CONFIG"
|
||||
|
||||
# Reset PostgreSQL database
|
||||
echo "🔄 Resetting PostgreSQL database..."
|
||||
DOCKER_COMPOSE_FILE="$REPO_ROOT/.github/workflows/configs/docker-compose.yml"
|
||||
|
||||
if [ -f "$DOCKER_COMPOSE_FILE" ]; then
|
||||
POSTGRES_CONTAINER=$(docker compose -f "$DOCKER_COMPOSE_FILE" ps -q postgres 2>/dev/null || true)
|
||||
|
||||
if [ -n "$POSTGRES_CONTAINER" ]; then
|
||||
docker exec "$POSTGRES_CONTAINER" \
|
||||
psql -U "$POSTGRES_USER" -d postgres -c "DROP DATABASE IF EXISTS $POSTGRES_DB; CREATE DATABASE $POSTGRES_DB;" \
|
||||
2>/dev/null || echo " ⚠️ Could not reset database (container may not be running)"
|
||||
echo " ✅ Database reset complete"
|
||||
else
|
||||
echo " ⚠️ PostgreSQL container not found, skipping database reset"
|
||||
fi
|
||||
else
|
||||
echo " ⚠️ Docker compose file not found, skipping database reset"
|
||||
fi
|
||||
|
||||
# Start Bifrost server
|
||||
echo "🚀 Starting Bifrost server..."
|
||||
SERVER_LOG="$TEMP_DIR/server.log"
|
||||
|
||||
"$BIFROST_BINARY" --app-dir "$TEMP_DIR" --port "$PORT" --log-level debug > "$SERVER_LOG" 2>&1 &
|
||||
BIFROST_PID=$!
|
||||
|
||||
echo " Started Bifrost with PID: $BIFROST_PID"
|
||||
|
||||
# Wait for server to be ready
|
||||
echo "⏳ Waiting for Bifrost to start..."
|
||||
MAX_WAIT=60
|
||||
ELAPSED=0
|
||||
SERVER_READY=false
|
||||
|
||||
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||
if grep -q "successfully started bifrost" "$SERVER_LOG" 2>/dev/null; then
|
||||
SERVER_READY=true
|
||||
echo " ✅ Bifrost started successfully"
|
||||
break
|
||||
fi
|
||||
|
||||
# Check if server process is still running
|
||||
if ! kill -0 "$BIFROST_PID" 2>/dev/null; then
|
||||
echo " ❌ Bifrost process died unexpectedly"
|
||||
echo " Server log:"
|
||||
cat "$SERVER_LOG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
ELAPSED=$((ELAPSED + 1))
|
||||
done
|
||||
|
||||
if [ "$SERVER_READY" = false ]; then
|
||||
echo " ❌ Bifrost failed to start within ${MAX_WAIT}s"
|
||||
echo " Server log:"
|
||||
cat "$SERVER_LOG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set environment variable for tests
|
||||
export BIFROST_BASE_URL="http://localhost:$PORT"
|
||||
echo " BIFROST_BASE_URL=$BIFROST_BASE_URL"
|
||||
|
||||
# Run Python integration tests
|
||||
echo ""
|
||||
echo "🧪 Running Python integration tests..."
|
||||
echo "="
|
||||
|
||||
cd "$REPO_ROOT/tests/integrations"
|
||||
|
||||
# Check if uv is available
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
echo "📦 Installing dependencies with uv..."
|
||||
uv sync --frozen --quiet
|
||||
|
||||
echo ""
|
||||
echo "🏃 Running tests..."
|
||||
TEST_EXIT_CODE=0
|
||||
uv run python run_all_tests.py --verbose || TEST_EXIT_CODE=$?
|
||||
else
|
||||
echo "⚠️ uv not found, trying pip..."
|
||||
|
||||
# Create virtual environment if needed
|
||||
if [ ! -d ".venv" ]; then
|
||||
python3 -m venv .venv
|
||||
fi
|
||||
|
||||
source .venv/bin/activate
|
||||
pip install -q -e .
|
||||
|
||||
echo ""
|
||||
echo "🏃 Running tests..."
|
||||
TEST_EXIT_CODE=0
|
||||
python run_all_tests.py --verbose || TEST_EXIT_CODE=$?
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "="
|
||||
|
||||
if [ $TEST_EXIT_CODE -eq 0 ]; then
|
||||
echo "✅ All integration tests passed!"
|
||||
else
|
||||
echo "❌ Some integration tests failed (exit code: $TEST_EXIT_CODE)"
|
||||
fi
|
||||
|
||||
# Exit with test result code (cleanup trap will run)
|
||||
exit $TEST_EXIT_CODE
|
||||
|
||||
4226
.github/workflows/scripts/run-migration-tests.sh
vendored
Executable file
4226
.github/workflows/scripts/run-migration-tests.sh
vendored
Executable file
File diff suppressed because it is too large
Load Diff
172
.github/workflows/scripts/run-tests.sh
vendored
Executable file
172
.github/workflows/scripts/run-tests.sh
vendored
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Comprehensive test runner for Bifrost PR validation
|
||||
# This script runs all test suites to validate changes
|
||||
|
||||
echo "🧪 Starting Bifrost Test Suite..."
|
||||
echo "=================================="
|
||||
|
||||
# Color codes for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Track test results
|
||||
TESTS_PASSED=0
|
||||
TESTS_FAILED=0
|
||||
|
||||
# Function to report test result
|
||||
report_result() {
|
||||
local test_name=$1
|
||||
local result=$2
|
||||
|
||||
if [ "$result" -eq 0 ]; then
|
||||
echo -e "${GREEN}✅ $test_name passed${NC}"
|
||||
((TESTS_PASSED++))
|
||||
else
|
||||
echo -e "${RED}❌ $test_name failed${NC}"
|
||||
((TESTS_FAILED++))
|
||||
fi
|
||||
}
|
||||
|
||||
# 1. Core Build Validation
|
||||
echo ""
|
||||
echo "📦 1/5 - Validating Core Build..."
|
||||
echo "-----------------------------------"
|
||||
cd core
|
||||
if go mod download && go build ./...; then
|
||||
report_result "Core Build" 0
|
||||
else
|
||||
report_result "Core Build" 1
|
||||
fi
|
||||
cd ..
|
||||
|
||||
# 2. Build MCP Test Servers
|
||||
echo ""
|
||||
echo "🔌 2/5 - Building MCP Test Servers..."
|
||||
echo "-----------------------------------"
|
||||
MCP_BUILD_FAILED=0
|
||||
for mcp_dir in examples/mcps/*/; do
|
||||
if [ -d "$mcp_dir" ]; then
|
||||
mcp_name=$(basename "$mcp_dir")
|
||||
if [ -f "$mcp_dir/go.mod" ]; then
|
||||
echo " Building $mcp_name (Go)..."
|
||||
mkdir -p "$mcp_dir/bin"
|
||||
if cd "$mcp_dir" && GOWORK=off go build -o "bin/$mcp_name" . && cd - > /dev/null; then
|
||||
echo -e " ${GREEN}✓ $mcp_name${NC}"
|
||||
else
|
||||
echo -e " ${RED}✗ $mcp_name${NC}"
|
||||
MCP_BUILD_FAILED=1
|
||||
cd - > /dev/null 2>&1 || true
|
||||
fi
|
||||
elif [ -f "$mcp_dir/package.json" ]; then
|
||||
echo " Building $mcp_name (TypeScript)..."
|
||||
if cd "$mcp_dir" && npm install --silent && npm run build && cd - > /dev/null; then
|
||||
echo -e " ${GREEN}✓ $mcp_name${NC}"
|
||||
else
|
||||
echo -e " ${RED}✗ $mcp_name${NC}"
|
||||
MCP_BUILD_FAILED=1
|
||||
cd - > /dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
report_result "MCP Test Servers Build" $MCP_BUILD_FAILED
|
||||
|
||||
# 3. Core Provider Tests
|
||||
echo ""
|
||||
echo "🔧 3/5 - Running Core Provider Tests..."
|
||||
echo "-----------------------------------"
|
||||
cd core
|
||||
if go test -v -run . ./...; then
|
||||
report_result "Core Provider Tests" 0
|
||||
else
|
||||
report_result "Core Provider Tests" 1
|
||||
fi
|
||||
cd ..
|
||||
|
||||
# 4. Governance Tests
|
||||
echo ""
|
||||
echo "🛡️ 4/5 - Running Governance Tests..."
|
||||
echo "-----------------------------------"
|
||||
if [ -d "tests/governance" ]; then
|
||||
cd tests/governance
|
||||
|
||||
# Check if virtual environment exists, create if not
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "Creating Python virtual environment..."
|
||||
python3 -m venv venv
|
||||
fi
|
||||
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing Python dependencies..."
|
||||
pip install -q -r requirements.txt
|
||||
|
||||
# Run tests
|
||||
if pytest -v; then
|
||||
report_result "Governance Tests" 0
|
||||
else
|
||||
report_result "Governance Tests" 1
|
||||
fi
|
||||
|
||||
deactivate
|
||||
cd ../..
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Governance tests directory not found, skipping...${NC}"
|
||||
fi
|
||||
|
||||
# 5. Integration Tests
|
||||
echo ""
|
||||
echo "🔗 5/5 - Running Integration Tests..."
|
||||
echo "-----------------------------------"
|
||||
if [ -d "tests/integrations" ]; then
|
||||
cd tests/integrations
|
||||
|
||||
# Check if virtual environment exists, create if not
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "Creating Python virtual environment..."
|
||||
python3 -m venv venv
|
||||
fi
|
||||
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing Python dependencies..."
|
||||
pip install -q -r requirements.txt
|
||||
|
||||
# Run tests
|
||||
if python run_all_tests.py; then
|
||||
report_result "Integration Tests" 0
|
||||
else
|
||||
report_result "Integration Tests" 1
|
||||
fi
|
||||
|
||||
deactivate
|
||||
cd ../..
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Integration tests directory not found, skipping...${NC}"
|
||||
fi
|
||||
|
||||
# Final Summary
|
||||
echo ""
|
||||
echo "=================================="
|
||||
echo "🏁 Test Suite Complete!"
|
||||
echo "=================================="
|
||||
echo -e "${GREEN}Passed: $TESTS_PASSED${NC}"
|
||||
echo -e "${RED}Failed: $TESTS_FAILED${NC}"
|
||||
echo ""
|
||||
|
||||
if [ "$TESTS_FAILED" -gt 0 ]; then
|
||||
echo -e "${RED}❌ Some tests failed. Please review the output above.${NC}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}✅ All tests passed successfully!${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
10
.github/workflows/scripts/schemasync/go.mod
vendored
Normal file
10
.github/workflows/scripts/schemasync/go.mod
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
module github.com/maximhq/bifrost/tools/schema-sync
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require golang.org/x/tools v0.30.0
|
||||
|
||||
require (
|
||||
golang.org/x/mod v0.23.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
)
|
||||
8
.github/workflows/scripts/schemasync/go.sum
vendored
Normal file
8
.github/workflows/scripts/schemasync/go.sum
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
1075
.github/workflows/scripts/schemasync/main.go
vendored
Normal file
1075
.github/workflows/scripts/schemasync/main.go
vendored
Normal file
File diff suppressed because it is too large
Load Diff
35
.github/workflows/scripts/setup-go-workspace.sh
vendored
Executable file
35
.github/workflows/scripts/setup-go-workspace.sh
vendored
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export GOTOOLCHAIN=auto
|
||||
|
||||
# If go.work exists, skip
|
||||
if [ -f "go.work" ]; then
|
||||
echo "🔍 Go workspace already exists, skipping initialization"
|
||||
return
|
||||
fi
|
||||
|
||||
|
||||
# Setup Go workspace for CI
|
||||
# Usage: source setup-go-workspace.sh
|
||||
echo "🔧 Setting up Go workspace..."
|
||||
if [ -f "go.work" ]; then
|
||||
echo "✅ Go workspace already exists, skipping init"
|
||||
return 0 2>/dev/null || exit 0
|
||||
fi
|
||||
go work init
|
||||
go work use ./core
|
||||
go work use ./framework
|
||||
go work use ./plugins/compat
|
||||
go work use ./plugins/governance
|
||||
go work use ./plugins/jsonparser
|
||||
go work use ./plugins/logging
|
||||
go work use ./plugins/maxim
|
||||
go work use ./plugins/mocker
|
||||
go work use ./plugins/otel
|
||||
go work use ./plugins/prompts
|
||||
go work use ./plugins/semanticcache
|
||||
go work use ./plugins/telemetry
|
||||
go work use ./transports
|
||||
go work use ./cli
|
||||
echo "✅ Go workspace initialized"
|
||||
179
.github/workflows/scripts/test-all-plugins.sh
vendored
Executable file
179
.github/workflows/scripts/test-all-plugins.sh
vendored
Executable file
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Test all plugins
|
||||
# Usage: ./test-all-plugins.sh [<JSON_ARRAY_OF_PLUGINS>]
|
||||
# If no argument provided, tests all plugins in the plugins/ directory
|
||||
|
||||
# Setup Go workspace for CI
|
||||
source "$(dirname "$0")/setup-go-workspace.sh"
|
||||
|
||||
echo "🧪 Running plugin tests..."
|
||||
|
||||
# Cleanup function to ensure Docker services are stopped
|
||||
cleanup_docker() {
|
||||
echo "🧹 Cleaning up Docker services..."
|
||||
if command -v docker-compose >/dev/null 2>&1; then
|
||||
docker-compose -f tests/docker-compose.yml down 2>/dev/null || true
|
||||
elif docker compose version >/dev/null 2>&1; then
|
||||
docker compose -f tests/docker-compose.yml down 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Register cleanup handler to run on script exit (success or failure)
|
||||
trap cleanup_docker EXIT
|
||||
|
||||
# Starting dependencies of plugin tests
|
||||
echo "🔧 Starting dependencies of plugin tests..."
|
||||
# Use docker compose (v2) if available, fallback to docker-compose (v1)
|
||||
if command -v docker-compose >/dev/null 2>&1; then
|
||||
docker-compose -f tests/docker-compose.yml up -d
|
||||
elif docker compose version >/dev/null 2>&1; then
|
||||
docker compose -f tests/docker-compose.yml up -d
|
||||
else
|
||||
echo "❌ Neither docker-compose nor docker compose is available"
|
||||
exit 1
|
||||
fi
|
||||
sleep 20
|
||||
|
||||
# Determine which plugins to test
|
||||
if [ $# -gt 0 ] && [ -n "$1" ]; then
|
||||
CHANGED_PLUGINS_JSON="$1"
|
||||
|
||||
# Verify jq is available
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "❌ Error: jq is required but not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate that the input is valid JSON
|
||||
if ! echo "$CHANGED_PLUGINS_JSON" | jq empty >/dev/null 2>&1; then
|
||||
echo "❌ Error: Invalid JSON provided"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# No work early‐exit if array is empty
|
||||
if jq -e 'length==0' <<<"$CHANGED_PLUGINS_JSON" >/dev/null 2>&1; then
|
||||
echo "⏭️ No plugins to test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Convert JSON array to bash array
|
||||
if ! readarray -t PLUGINS < <(echo "$CHANGED_PLUGINS_JSON" | jq -r '.[]' 2>/dev/null); then
|
||||
echo "❌ Error: Failed to parse plugin names from JSON"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Test all plugins in the plugins/ directory
|
||||
PLUGINS=()
|
||||
for plugin_dir in plugins/*/; do
|
||||
if [ -d "$plugin_dir" ] && [ -f "$plugin_dir/go.mod" ]; then
|
||||
plugin_name=$(basename "$plugin_dir")
|
||||
PLUGINS+=("$plugin_name")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ ${#PLUGINS[@]} -eq 0 ]; then
|
||||
echo "⏭️ No plugins to test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "🔌 Testing ${#PLUGINS[@]} plugins:"
|
||||
for p in "${PLUGINS[@]}"; do
|
||||
echo " • $p"
|
||||
done
|
||||
|
||||
FAILED_PLUGINS=()
|
||||
SUCCESS_COUNT=0
|
||||
OVERALL_EXIT_CODE=0
|
||||
|
||||
# Test each plugin
|
||||
for plugin in "${PLUGINS[@]}"; do
|
||||
echo ""
|
||||
echo "🔌 Testing plugin: $plugin"
|
||||
|
||||
PLUGIN_DIR="plugins/$plugin"
|
||||
|
||||
if [ ! -d "$PLUGIN_DIR" ]; then
|
||||
echo "⚠️ Warning: Plugin directory not found: $PLUGIN_DIR (skipping)"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ ! -f "$PLUGIN_DIR/go.mod" ]; then
|
||||
echo "ℹ️ No go.mod found for $plugin, skipping tests"
|
||||
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
cd "$PLUGIN_DIR"
|
||||
|
||||
# Validate build
|
||||
echo "🔨 Validating plugin build..."
|
||||
if ! go build ./...; then
|
||||
echo "❌ Build failed for plugin: $plugin"
|
||||
FAILED_PLUGINS+=("$plugin")
|
||||
OVERALL_EXIT_CODE=1
|
||||
cd ../..
|
||||
continue
|
||||
fi
|
||||
|
||||
# Run tests with coverage if any exist
|
||||
if go list ./... | grep -q .; then
|
||||
# Run E2E tests for governance plugin (currently disabled)
|
||||
if [ "$plugin" = "governance" ]; then
|
||||
echo "🧪 Running governance plugin tests..."
|
||||
# Governance plugin tests are currently disabled in release script
|
||||
# Just run regular tests
|
||||
if go test -v -timeout 20m -coverprofile=coverage.txt -coverpkg=./... ./...; then
|
||||
echo "✅ Tests passed for: $plugin"
|
||||
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||
else
|
||||
echo "❌ Tests failed for: $plugin"
|
||||
FAILED_PLUGINS+=("$plugin")
|
||||
OVERALL_EXIT_CODE=1
|
||||
fi
|
||||
else
|
||||
echo "🧪 Running plugin tests with coverage..."
|
||||
if go test -v -timeout 20m -coverprofile=coverage.txt -coverpkg=./... ./...; then
|
||||
echo "✅ Tests passed for: $plugin"
|
||||
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||
else
|
||||
echo "❌ Tests failed for: $plugin"
|
||||
FAILED_PLUGINS+=("$plugin")
|
||||
OVERALL_EXIT_CODE=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Upload coverage to Codecov
|
||||
if [ -n "${CODECOV_TOKEN:-}" ] && [ -f coverage.txt ]; then
|
||||
echo "📊 Uploading coverage to Codecov..."
|
||||
curl -Os https://uploader.codecov.io/latest/linux/codecov
|
||||
chmod +x codecov
|
||||
./codecov -t "$CODECOV_TOKEN" -f coverage.txt -F "plugin-${plugin}"
|
||||
rm -f codecov coverage.txt
|
||||
else
|
||||
rm -f coverage.txt
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ No tests found for $plugin"
|
||||
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||
fi
|
||||
|
||||
cd ../..
|
||||
done
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "📋 Plugin Test Summary:"
|
||||
echo " ✅ Successful: $SUCCESS_COUNT/${#PLUGINS[@]}"
|
||||
echo " ❌ Failed: ${#FAILED_PLUGINS[@]}"
|
||||
|
||||
if [ ${#FAILED_PLUGINS[@]} -gt 0 ]; then
|
||||
echo " Failed plugins: ${FAILED_PLUGINS[*]}"
|
||||
echo "❌ Plugin tests completed with failures"
|
||||
exit $OVERALL_EXIT_CODE
|
||||
else
|
||||
echo " 🎉 All plugin tests passed!"
|
||||
echo "✅ Plugin tests completed successfully"
|
||||
fi
|
||||
236
.github/workflows/scripts/test-bifrost-http.sh
vendored
Executable file
236
.github/workflows/scripts/test-bifrost-http.sh
vendored
Executable file
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Test bifrost-http component
|
||||
# Usage: ./test-bifrost-http.sh
|
||||
|
||||
# Get the absolute path of the script directory
|
||||
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
else
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
fi
|
||||
|
||||
# Setup Go workspace for CI
|
||||
source "$(dirname "$0")/setup-go-workspace.sh"
|
||||
|
||||
echo "🧪 Running bifrost-http tests..."
|
||||
|
||||
# Validate that config.schema.json and values.schema.json are in sync
|
||||
echo "🔍 Validating schema consistency between config.schema.json and values.schema.json..."
|
||||
VALIDATE_SCHEMA_SCRIPT="$SCRIPT_DIR/validate-helm-schema.sh"
|
||||
if [ -f "$VALIDATE_SCHEMA_SCRIPT" ]; then
|
||||
if ! "$VALIDATE_SCHEMA_SCRIPT"; then
|
||||
echo "❌ Schema validation failed. The Helm chart values.schema.json is not in sync with config.schema.json"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Schema validation passed"
|
||||
else
|
||||
echo "⚠️ Warning: validate-helm-schema.sh not found, skipping schema validation"
|
||||
fi
|
||||
|
||||
# Cleanup function to ensure Docker services are stopped
|
||||
cleanup_docker() {
|
||||
echo "🧹 Cleaning up Docker services..."
|
||||
docker compose -f "$CONFIGS_DIR/docker-compose.yml" down 2>/dev/null || true
|
||||
}
|
||||
|
||||
CONFIGS_DIR=".github/workflows/configs"
|
||||
|
||||
# Register cleanup handler to run on script exit (success or failure)
|
||||
trap cleanup_docker EXIT
|
||||
|
||||
# Build UI first before we can validate the transport build
|
||||
echo "🎨 Building UI..."
|
||||
make build-ui
|
||||
|
||||
# Building hello-world plugin
|
||||
echo "🔨 Building hello-world plugin..."
|
||||
cd examples/plugins/hello-world
|
||||
make build
|
||||
cd ../../..
|
||||
|
||||
# Validate transport build
|
||||
echo "🔨 Validating transport build..."
|
||||
cd transports
|
||||
go build ./...
|
||||
|
||||
# Run unit tests with coverage
|
||||
echo "🧪 Running unit tests with coverage..."
|
||||
go test --race -v -timeout 40m -coverprofile=coverage.txt ./...
|
||||
|
||||
# Upload coverage to Codecov
|
||||
if [ -n "${CODECOV_TOKEN:-}" ]; then
|
||||
echo "📊 Uploading coverage to Codecov..."
|
||||
curl -Os https://uploader.codecov.io/latest/linux/codecov
|
||||
chmod +x codecov
|
||||
./codecov -t "$CODECOV_TOKEN" -f coverage.txt -F transports
|
||||
rm -f codecov coverage.txt
|
||||
else
|
||||
echo "ℹ️ CODECOV_TOKEN not set, skipping coverage upload"
|
||||
rm -f coverage.txt
|
||||
fi
|
||||
|
||||
# Build the binary for integration testing
|
||||
echo "🔨 Building binary for integration testing..."
|
||||
mkdir -p ../tmp
|
||||
cd bifrost-http
|
||||
go build -o ../../tmp/bifrost-http .
|
||||
cd ..
|
||||
|
||||
# Run integration tests with different configurations
|
||||
echo "🧪 Running integration tests with different configurations..."
|
||||
CONFIGS_TO_TEST=(
|
||||
"default"
|
||||
"emptystate"
|
||||
"noconfigstorenologstore"
|
||||
"witconfigstorelogstorepostgres"
|
||||
"withconfigstore"
|
||||
"withconfigstorelogsstorepostgres"
|
||||
"withconfigstorelogsstoresqlite"
|
||||
"withdynamicplugin"
|
||||
"withobservability"
|
||||
"withsemanticcache"
|
||||
"withpostgresmcpclientsinconfig"
|
||||
)
|
||||
|
||||
TEST_BINARY="../tmp/bifrost-http"
|
||||
CONFIGS_DIR="../.github/workflows/configs"
|
||||
|
||||
# Running docker compose
|
||||
echo "🐳 Starting Docker services (PostgreSQL, Weaviate, Redis)..."
|
||||
docker compose -f "$CONFIGS_DIR/docker-compose.yml" up -d
|
||||
|
||||
# Wait for services to be healthy with polling
|
||||
echo "⏳ Waiting for Docker services to be ready..."
|
||||
MAX_WAIT=300
|
||||
ELAPSED=0
|
||||
SERVICES_READY=false
|
||||
|
||||
# Get expected number of services
|
||||
EXPECTED_SERVICES=$(docker compose -f "$CONFIGS_DIR/docker-compose.yml" config --services 2>/dev/null | wc -l | tr -d ' ')
|
||||
|
||||
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||
# Get running container count
|
||||
RUNNING_COUNT=$(docker compose -f "$CONFIGS_DIR/docker-compose.yml" ps --status running -q 2>/dev/null | wc -l | tr -d ' ')
|
||||
|
||||
# Check health status: count healthy and unhealthy (starting/unhealthy) services
|
||||
HEALTH_OUTPUT=$(docker compose -f "$CONFIGS_DIR/docker-compose.yml" ps --format "{{.Name}}:{{.Health}}" 2>/dev/null)
|
||||
HEALTHY_COUNT=$(echo "$HEALTH_OUTPUT" | grep -c ":healthy") || HEALTHY_COUNT=0
|
||||
UNHEALTHY_COUNT=$(echo "$HEALTH_OUTPUT" | grep -cE ":(starting|unhealthy)") || UNHEALTHY_COUNT=0
|
||||
|
||||
# All services are ready when:
|
||||
# 1. All expected services are running
|
||||
# 2. No services are in "starting" or "unhealthy" state
|
||||
if [ "$RUNNING_COUNT" -eq "$EXPECTED_SERVICES" ] && [ "$UNHEALTHY_COUNT" -eq "0" ]; then
|
||||
SERVICES_READY=true
|
||||
echo "✅ All Docker services are ready ($HEALTHY_COUNT with healthchecks, ${ELAPSED}s)"
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
ELAPSED=$((ELAPSED + 2))
|
||||
echo " ⏳ Waiting for services... ($RUNNING_COUNT/$EXPECTED_SERVICES running, $HEALTHY_COUNT healthy, $UNHEALTHY_COUNT starting, ${ELAPSED}s/${MAX_WAIT}s)"
|
||||
done
|
||||
|
||||
if [ "$SERVICES_READY" = false ]; then
|
||||
echo "❌ Docker services failed to become healthy within ${MAX_WAIT}s"
|
||||
echo " Current service status:"
|
||||
docker compose -f "$CONFIGS_DIR/docker-compose.yml" ps
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for config in "${CONFIGS_TO_TEST[@]}"; do
|
||||
echo " 🔍 Testing with config: $config"
|
||||
config_path="$CONFIGS_DIR/$config"
|
||||
|
||||
# Clean up databases before each config test for a clean slate
|
||||
echo " 🧹 Resetting PostgreSQL database..."
|
||||
docker exec -e PGPASSWORD=bifrost_password "$(docker compose -f "$CONFIGS_DIR/docker-compose.yml" ps -q postgres)" \
|
||||
psql -U bifrost -d postgres \
|
||||
-c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'bifrost' AND pid <> pg_backend_pid();" \
|
||||
-c "DROP DATABASE IF EXISTS bifrost;" \
|
||||
-c "CREATE DATABASE bifrost;"
|
||||
|
||||
echo " 🧹 Cleaning up SQLite database files for config: $config..."
|
||||
find "$config_path" -type f \( -name "*.db" -o -name "*.db-shm" -o -name "*.db-wal" \) -delete 2>/dev/null || true
|
||||
echo " ✅ Database cleanup complete"
|
||||
|
||||
if [ ! -d "$config_path" ]; then
|
||||
echo " ⚠️ Warning: Config directory not found: $config_path (skipping)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Create a temporary log file for server output
|
||||
SERVER_LOG=$(mktemp)
|
||||
|
||||
# Start the server in background with a timeout, logging to file and console
|
||||
timeout 120s $TEST_BINARY --app-dir "$config_path" --port 18080 --log-level debug 2>&1 | tee "$SERVER_LOG" &
|
||||
SERVER_PID=$!
|
||||
|
||||
# Wait for server to be ready by looking for the startup message
|
||||
echo " ⏳ Waiting for server to start..."
|
||||
MAX_WAIT=30
|
||||
ELAPSED=0
|
||||
SERVER_READY=false
|
||||
|
||||
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||
if grep -q "successfully started bifrost, serving UI on http://localhost:18080" "$SERVER_LOG" 2>/dev/null; then
|
||||
SERVER_READY=true
|
||||
echo " ✅ Server started successfully with config: $config"
|
||||
break
|
||||
fi
|
||||
|
||||
# Check if server process is still running
|
||||
if ! kill -0 $SERVER_PID 2>/dev/null; then
|
||||
echo " ❌ Server process died before starting with config: $config"
|
||||
rm -f "$SERVER_LOG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
ELAPSED=$((ELAPSED + 1))
|
||||
done
|
||||
|
||||
if [ "$SERVER_READY" = false ]; then
|
||||
echo " ❌ Server failed to start within ${MAX_WAIT}s with config: $config"
|
||||
kill $SERVER_PID 2>/dev/null || true
|
||||
wait $SERVER_PID 2>/dev/null || true
|
||||
rm -f "$SERVER_LOG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run get_curls.sh to test all GET endpoints
|
||||
echo " 🧪 Running API endpoint tests..."
|
||||
GET_CURLS_SCRIPT="$SCRIPT_DIR/get_curls.sh"
|
||||
|
||||
if [ -f "$GET_CURLS_SCRIPT" ]; then
|
||||
BASE_URL="http://localhost:18080" "$GET_CURLS_SCRIPT"
|
||||
CURL_EXIT_CODE=$?
|
||||
|
||||
if [ $CURL_EXIT_CODE -eq 0 ]; then
|
||||
echo " ✅ API endpoint tests passed for config: $config"
|
||||
else
|
||||
echo " ❌ API endpoint tests failed for config: $config (exit code: $CURL_EXIT_CODE)"
|
||||
kill $SERVER_PID 2>/dev/null || true
|
||||
wait $SERVER_PID 2>/dev/null || true
|
||||
rm -f "$SERVER_LOG"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo " ⚠️ Warning: get_curls.sh not found at $GET_CURLS_SCRIPT (skipping endpoint tests)"
|
||||
fi
|
||||
|
||||
# Kill the server
|
||||
kill $SERVER_PID 2>/dev/null || true
|
||||
wait $SERVER_PID 2>/dev/null || true
|
||||
|
||||
# Clean up log file
|
||||
rm -f "$SERVER_LOG"
|
||||
|
||||
# Clean up any lingering processes
|
||||
sleep 1
|
||||
done
|
||||
|
||||
cd ..
|
||||
echo "✅ Bifrost-HTTP tests completed successfully"
|
||||
57
.github/workflows/scripts/test-core.sh
vendored
Executable file
57
.github/workflows/scripts/test-core.sh
vendored
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Test core component
|
||||
# Usage: ./test-core.sh
|
||||
|
||||
# Setup Go workspace for CI
|
||||
source "$(dirname "$0")/setup-go-workspace.sh"
|
||||
|
||||
echo "🧪 Running core tests..."
|
||||
|
||||
# Build MCP test servers for STDIO tests
|
||||
echo "🔧 Building MCP test servers..."
|
||||
for mcp_dir in examples/mcps/*/; do
|
||||
if [ -d "$mcp_dir" ]; then
|
||||
mcp_name=$(basename "$mcp_dir")
|
||||
if [ -f "$mcp_dir/go.mod" ]; then
|
||||
echo " Building $mcp_name (Go)..."
|
||||
mkdir -p "$mcp_dir/bin"
|
||||
pushd "$mcp_dir" > /dev/null
|
||||
GOWORK=off go build -o "bin/$mcp_name" .
|
||||
popd > /dev/null
|
||||
elif [ -f "$mcp_dir/package.json" ]; then
|
||||
echo " Building $mcp_name (TypeScript)..."
|
||||
pushd "$mcp_dir" > /dev/null
|
||||
npm install --silent && npm run build
|
||||
popd > /dev/null
|
||||
fi
|
||||
fi
|
||||
done
|
||||
echo "✅ MCP test servers built"
|
||||
|
||||
# Validate core build
|
||||
echo "🔨 Validating core build..."
|
||||
cd core
|
||||
go mod download
|
||||
go build ./...
|
||||
echo "✅ Core build validation successful"
|
||||
|
||||
# Run core tests with coverage
|
||||
echo "🧪 Running core tests with coverage..."
|
||||
go test -race -timeout 20m -coverprofile=coverage.txt -coverpkg=./... ./...
|
||||
|
||||
# Upload coverage to Codecov
|
||||
if [ -n "${CODECOV_TOKEN:-}" ]; then
|
||||
echo "📊 Uploading coverage to Codecov..."
|
||||
curl -Os https://uploader.codecov.io/latest/linux/codecov
|
||||
chmod +x codecov
|
||||
./codecov -t "$CODECOV_TOKEN" -f coverage.txt -F core
|
||||
rm -f codecov coverage.txt
|
||||
else
|
||||
echo "ℹ️ CODECOV_TOKEN not set, skipping coverage upload"
|
||||
rm -f coverage.txt
|
||||
fi
|
||||
cd ..
|
||||
|
||||
echo "✅ Core tests completed successfully"
|
||||
301
.github/workflows/scripts/test-docker-image.sh
vendored
Executable file
301
.github/workflows/scripts/test-docker-image.sh
vendored
Executable file
@@ -0,0 +1,301 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Test Docker image by building, starting with docker-compose, and running E2E API tests
|
||||
# Usage: ./test-docker-image.sh <platform>
|
||||
# Example: ./test-docker-image.sh linux/amd64
|
||||
|
||||
# Get the absolute path of the script directory
|
||||
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
else
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
fi
|
||||
|
||||
# Repository root (3 levels up from .github/workflows/scripts)
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd -P)"
|
||||
|
||||
|
||||
# Setup Go workspace for CI (go.work is gitignored, must be regenerated)
|
||||
source "$SCRIPT_DIR/setup-go-workspace.sh"
|
||||
|
||||
PLATFORM=${1:-linux/amd64}
|
||||
ARCH=$(echo "$PLATFORM" | cut -d'/' -f2)
|
||||
IMAGE_TAG="bifrost-test:ci-${GITHUB_SHA:-local}-${ARCH}"
|
||||
CONTAINER_NAME="bifrost-test-${ARCH}"
|
||||
TEST_PORT=8080
|
||||
DOCKER_COMPOSE_FILE="$REPO_ROOT/tests/docker-compose.yml"
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
CONFIG_FILE="$TEMP_DIR/config.json"
|
||||
|
||||
echo "=== Testing Docker image for ${PLATFORM} ==="
|
||||
|
||||
# Cleanup function
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
echo ""
|
||||
echo "=== Cleaning up ==="
|
||||
|
||||
# Stop and remove Bifrost container
|
||||
echo "Stopping Bifrost container..."
|
||||
docker stop "${CONTAINER_NAME}" > /dev/null 2>&1 || true
|
||||
docker rm "${CONTAINER_NAME}" > /dev/null 2>&1 || true
|
||||
|
||||
# Stop docker-compose services
|
||||
echo "Stopping docker-compose services..."
|
||||
docker compose -f "$DOCKER_COMPOSE_FILE" down -v > /dev/null 2>&1 || true
|
||||
|
||||
# Remove test image
|
||||
echo "Removing test image..."
|
||||
docker rmi "${IMAGE_TAG}" > /dev/null 2>&1 || true
|
||||
|
||||
# Remove temp directory
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
exit $exit_code
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Build the image using local module sources (pre-release CI builds)
|
||||
echo "Building Docker image (local modules)..."
|
||||
docker build \
|
||||
--platform "${PLATFORM}" \
|
||||
-f transports/Dockerfile.local \
|
||||
-t "${IMAGE_TAG}" \
|
||||
.
|
||||
|
||||
echo "Build complete: ${IMAGE_TAG}"
|
||||
|
||||
# Start docker-compose services (Postgres, Weaviate, Redis, Qdrant)
|
||||
echo ""
|
||||
echo "=== Starting docker-compose services ==="
|
||||
docker compose -f "$DOCKER_COMPOSE_FILE" up -d
|
||||
|
||||
# Wait for Postgres to be ready
|
||||
echo "Waiting for Postgres to be ready..."
|
||||
MAX_WAIT=60
|
||||
ELAPSED=0
|
||||
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||
if docker compose -f "$DOCKER_COMPOSE_FILE" exec -T postgres pg_isready -U bifrost -d bifrost > /dev/null 2>&1; then
|
||||
echo "Postgres is ready"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
ELAPSED=$((ELAPSED + 2))
|
||||
done
|
||||
|
||||
if [ $ELAPSED -ge $MAX_WAIT ]; then
|
||||
echo "ERROR: Postgres did not become ready within ${MAX_WAIT}s"
|
||||
docker compose -f "$DOCKER_COMPOSE_FILE" logs postgres
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the docker network name
|
||||
NETWORK_NAME=$(docker compose -f "$DOCKER_COMPOSE_FILE" ps --format json | head -1 | jq -r '.Networks' 2>/dev/null || echo "tests_bifrost_network")
|
||||
if [ -z "$NETWORK_NAME" ] || [ "$NETWORK_NAME" = "null" ]; then
|
||||
NETWORK_NAME="tests_bifrost_network"
|
||||
fi
|
||||
|
||||
# Generate config.json with all providers and Postgres stores
|
||||
echo ""
|
||||
echo "=== Generating config.json ==="
|
||||
cat > "$CONFIG_FILE" << 'CONFIGEOF'
|
||||
{
|
||||
"$schema": "https://www.getbifrost.ai/schema",
|
||||
"providers": {
|
||||
"openai": {
|
||||
"keys": [{ "name": "OpenAI API Key", "value": "env.OPENAI_API_KEY", "weight": 1, "use_for_batch_api": true }],
|
||||
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||
},
|
||||
"elevenlabs": {
|
||||
"keys": [{ "name": "ElevenLabs API Key", "value": "env.ELEVENLABS_API_KEY", "weight": 1, "use_for_batch_api": true }],
|
||||
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||
},
|
||||
"xai": {
|
||||
"keys": [{ "name": "Xai API Key", "value": "env.XAI_API_KEY", "weight": 1, "use_for_batch_api": true }],
|
||||
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||
},
|
||||
"huggingface": {
|
||||
"keys": [{ "name": "Hugging Face API Key", "value": "env.HUGGING_FACE_API_KEY", "weight": 1, "use_for_batch_api": true }],
|
||||
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||
},
|
||||
"anthropic": {
|
||||
"keys": [{ "name": "Anthropic API Key", "value": "env.ANTHROPIC_API_KEY", "weight": 1, "use_for_batch_api": true }],
|
||||
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||
},
|
||||
"gemini": {
|
||||
"keys": [{ "value": "env.GEMINI_API_KEY", "weight": 1, "use_for_batch_api": true, "name": "Gemini API Key" }],
|
||||
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||
},
|
||||
"vertex": {
|
||||
"keys": [{ "name": "Vertex API Key", "vertex_key_config": { "project_id": "env.VERTEX_PROJECT_ID", "region": "env.GOOGLE_LOCATION", "auth_credentials": "env.VERTEX_CREDENTIALS" }, "weight": 1 }],
|
||||
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||
},
|
||||
"mistral": {
|
||||
"keys": [{ "name": "Mistral API Key", "value": "env.MISTRAL_API_KEY", "weight": 1 }],
|
||||
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||
},
|
||||
"cohere": {
|
||||
"keys": [{ "name": "Cohere API Key", "value": "env.COHERE_API_KEY", "weight": 1 }],
|
||||
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||
},
|
||||
"groq": {
|
||||
"keys": [{ "name": "Groq API Key", "value": "env.GROQ_API_KEY", "weight": 1 }],
|
||||
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||
},
|
||||
"perplexity": {
|
||||
"keys": [{ "name": "Perplexity API Key", "value": "env.PERPLEXITY_API_KEY", "weight": 1 }],
|
||||
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||
},
|
||||
"cerebras": {
|
||||
"keys": [{ "name": "Cerebras API Key", "value": "env.CEREBRAS_API_KEY", "weight": 1 }],
|
||||
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||
},
|
||||
"openrouter": {
|
||||
"keys": [{ "name": "OpenRouter API Key", "value": "env.OPENROUTER_API_KEY", "weight": 1 }],
|
||||
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||
},
|
||||
"parasail": {
|
||||
"keys": [{ "name": "Parasail API Key", "value": "env.PARASAIL_API_KEY", "weight": 1 }],
|
||||
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||
},
|
||||
"azure": {
|
||||
"keys": [{ "name": "Azure API Key", "value": "env.AZURE_API_KEY", "azure_key_config": { "endpoint": "env.AZURE_ENDPOINT", "api_version": "env.AZURE_API_VERSION" }, "weight": 1 }],
|
||||
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||
},
|
||||
"bedrock": {
|
||||
"keys": [{ "name": "Bedrock API Key", "bedrock_key_config": { "access_key": "env.AWS_ACCESS_KEY_ID", "secret_key": "env.AWS_SECRET_ACCESS_KEY", "region": "env.AWS_REGION", "arn": "env.AWS_ARN" }, "weight": 1, "use_for_batch_api": true }],
|
||||
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||
},
|
||||
"replicate": {
|
||||
"keys": [{ "name": "Replicate API KEY", "value": "env.REPLICATE_API_KEY", "weight": 1.0, "use_for_batch_api": true }]
|
||||
}
|
||||
},
|
||||
"config_store": {
|
||||
"enabled": true,
|
||||
"type": "postgres",
|
||||
"config": {
|
||||
"host": "postgres",
|
||||
"port": "5432",
|
||||
"user": "bifrost",
|
||||
"password": "bifrost_password",
|
||||
"db_name": "bifrost",
|
||||
"ssl_mode": "disable"
|
||||
}
|
||||
},
|
||||
"logs_store": {
|
||||
"enabled": true,
|
||||
"type": "postgres",
|
||||
"config": {
|
||||
"host": "postgres",
|
||||
"port": "5432",
|
||||
"user": "bifrost",
|
||||
"password": "bifrost_password",
|
||||
"db_name": "bifrost",
|
||||
"ssl_mode": "disable"
|
||||
}
|
||||
},
|
||||
"governance": {
|
||||
"virtual_keys": [
|
||||
{
|
||||
"id": "vk-test",
|
||||
"value": "sk-bf-test-key",
|
||||
"is_active": true,
|
||||
"name": "vk-test"
|
||||
}
|
||||
]
|
||||
},
|
||||
"client": {
|
||||
"drop_excess_requests": false,
|
||||
"initial_pool_size": 300,
|
||||
"allowed_origins": ["http://localhost:3000", "https://localhost:3000"],
|
||||
"enable_logging": true,
|
||||
"enforce_governance_header": false,
|
||||
"allow_direct_keys": false,
|
||||
"max_request_body_size_mb": 100
|
||||
},
|
||||
"encryption_key": ""
|
||||
}
|
||||
CONFIGEOF
|
||||
|
||||
echo "Config file created at: $CONFIG_FILE"
|
||||
|
||||
# Run the Bifrost container connected to the docker-compose network
|
||||
echo ""
|
||||
echo "=== Starting Bifrost container ==="
|
||||
docker run -d \
|
||||
--name "${CONTAINER_NAME}" \
|
||||
--platform "${PLATFORM}" \
|
||||
--network "${NETWORK_NAME}" \
|
||||
-p ${TEST_PORT}:8080 \
|
||||
-e APP_PORT=8080 \
|
||||
-e APP_HOST=0.0.0.0 \
|
||||
-e OPENAI_API_KEY="${OPENAI_API_KEY:-}" \
|
||||
-e ELEVENLABS_API_KEY="${ELEVENLABS_API_KEY:-}" \
|
||||
-e XAI_API_KEY="${XAI_API_KEY:-}" \
|
||||
-e HUGGING_FACE_API_KEY="${HUGGING_FACE_API_KEY:-}" \
|
||||
-e ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-}" \
|
||||
-e GEMINI_API_KEY="${GEMINI_API_KEY:-}" \
|
||||
-e VERTEX_PROJECT_ID="${VERTEX_PROJECT_ID:-}" \
|
||||
-e VERTEX_CREDENTIALS="${VERTEX_CREDENTIALS:-}" \
|
||||
-e GOOGLE_LOCATION="${GOOGLE_LOCATION:-us-central1}" \
|
||||
-e MISTRAL_API_KEY="${MISTRAL_API_KEY:-}" \
|
||||
-e COHERE_API_KEY="${COHERE_API_KEY:-}" \
|
||||
-e GROQ_API_KEY="${GROQ_API_KEY:-}" \
|
||||
-e PERPLEXITY_API_KEY="${PERPLEXITY_API_KEY:-}" \
|
||||
-e CEREBRAS_API_KEY="${CEREBRAS_API_KEY:-}" \
|
||||
-e OPENROUTER_API_KEY="${OPENROUTER_API_KEY:-}" \
|
||||
-e PARASAIL_API_KEY="${PARASAIL_API_KEY:-}" \
|
||||
-e AZURE_API_KEY="${AZURE_API_KEY:-}" \
|
||||
-e AZURE_ENDPOINT="${AZURE_ENDPOINT:-}" \
|
||||
-e AZURE_API_VERSION="${AZURE_API_VERSION:-}" \
|
||||
-e AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}" \
|
||||
-e AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}" \
|
||||
-e AWS_REGION="${AWS_REGION:-us-east-1}" \
|
||||
-e AWS_ARN="${AWS_ARN:-}" \
|
||||
-e REPLICATE_API_KEY="${REPLICATE_API_KEY:-}" \
|
||||
-v "$CONFIG_FILE:/app/data/config.json:ro" \
|
||||
"${IMAGE_TAG}"
|
||||
|
||||
# Wait for Bifrost to be ready
|
||||
echo "Waiting for Bifrost to start..."
|
||||
MAX_WAIT=60
|
||||
ELAPSED=0
|
||||
HEALTH_OK=0
|
||||
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||
if curl -sf "http://localhost:${TEST_PORT}/health" > /dev/null 2>&1; then
|
||||
echo "Bifrost health check passed (attempt $((ELAPSED/2 + 1)))"
|
||||
HEALTH_OK=1
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
ELAPSED=$((ELAPSED + 2))
|
||||
done
|
||||
|
||||
if [ $HEALTH_OK -eq 0 ]; then
|
||||
echo "ERROR: Bifrost health check failed!"
|
||||
echo "Container logs:"
|
||||
docker logs "${CONTAINER_NAME}" 2>&1 | tail -100 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# # Run E2E API tests
|
||||
# echo ""
|
||||
# echo "=== Running E2E API tests ==="
|
||||
# export BIFROST_BASE_URL="http://localhost:${TEST_PORT}"
|
||||
# export CI=1
|
||||
|
||||
# echo pwd: $(pwd)
|
||||
# # Run the E2E API test scripts (marked as flaky - failures are logged but don't block)
|
||||
# if ! ./tests/e2e/api/runners/run-newman-inference-tests.sh; then
|
||||
# echo "WARNING: runners/run-newman-inference-tests.sh failed (flaky test - continuing)"
|
||||
# fi
|
||||
# if ! ./tests/e2e/api/run-all-integrations.sh; then
|
||||
# echo "WARNING: run-all-integrations.sh failed (flaky test - continuing)"
|
||||
# fi
|
||||
# if ! ./tests/e2e/api/runners/run-newman-api-tests.sh; then
|
||||
# echo "WARNING: run-newman-api-tests.sh failed (flaky test - continuing)"
|
||||
# fi
|
||||
|
||||
# echo ""
|
||||
# echo "=== Docker image E2E API test passed for ${PLATFORM} ==="
|
||||
180
.github/workflows/scripts/test-e2e-api.sh
vendored
Executable file
180
.github/workflows/scripts/test-e2e-api.sh
vendored
Executable file
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# E2E API tests: /v1, /integrations, /api (Newman/Postman).
|
||||
# Usage:
|
||||
# ./test-e2e-api.sh # Bifrost already running at BIFROST_BASE_URL
|
||||
# ./test-e2e-api.sh <bifrost-binary> [port] # Start Bifrost with config, then run tests
|
||||
# Config: tests/integrations/python/config.json (merged with Postgres when starting server)
|
||||
# Requires: Newman installed; provider API keys in environment when starting Bifrost
|
||||
|
||||
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
else
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
fi
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd -P)"
|
||||
E2E_API_DIR="$REPO_ROOT/tests/e2e/api"
|
||||
E2E_API_CONFIG="$REPO_ROOT/tests/integrations/python/config.json"
|
||||
|
||||
export BIFROST_BASE_URL="${BIFROST_BASE_URL:-http://localhost:8080}"
|
||||
|
||||
# ----- Optional: start Bifrost if binary path given -----
|
||||
if [ -n "${1:-}" ]; then
|
||||
BIFROST_BINARY="$1"
|
||||
PORT="${2:-8080}"
|
||||
POSTGRES_HOST="${POSTGRES_HOST:-localhost}"
|
||||
POSTGRES_PORT="${POSTGRES_PORT:-5432}"
|
||||
POSTGRES_USER="${POSTGRES_USER:-bifrost}"
|
||||
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-bifrost_password}"
|
||||
POSTGRES_DB="${POSTGRES_DB:-bifrost}"
|
||||
POSTGRES_SSLMODE="${POSTGRES_SSLMODE:-disable}"
|
||||
export POSTGRES_HOST POSTGRES_PORT POSTGRES_USER POSTGRES_PASSWORD POSTGRES_DB POSTGRES_SSLMODE
|
||||
|
||||
if [ ! -f "$BIFROST_BINARY" ] || [ ! -x "$BIFROST_BINARY" ]; then
|
||||
echo "❌ Bifrost binary not found or not executable: $BIFROST_BINARY" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$E2E_API_CONFIG" ]; then
|
||||
echo "❌ Config not found: $E2E_API_CONFIG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
MERGED_CONFIG="$TEMP_DIR/config.json"
|
||||
SERVER_LOG="$TEMP_DIR/server.log"
|
||||
BIFROST_PID=""
|
||||
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
if [ -n "${BIFROST_PID:-}" ] && kill -0 "$BIFROST_PID" 2>/dev/null; then
|
||||
kill "$BIFROST_PID" 2>/dev/null || true
|
||||
wait "$BIFROST_PID" 2>/dev/null || true
|
||||
fi
|
||||
rm -rf "$TEMP_DIR"
|
||||
exit $exit_code
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "📝 Merged config (providers + Postgres)..."
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
jq --arg host "$POSTGRES_HOST" --arg port "$POSTGRES_PORT" --arg user "$POSTGRES_USER" \
|
||||
--arg pass "$POSTGRES_PASSWORD" --arg db "$POSTGRES_DB" --arg ssl "$POSTGRES_SSLMODE" \
|
||||
'. + {
|
||||
"config_store": {"enabled": true, "type": "postgres", "config": {"host": $host, "port": $port, "user": $user, "password": $pass, "db_name": $db, "ssl_mode": $ssl}},
|
||||
"logs_store": {"enabled": true, "type": "postgres", "config": {"host": $host, "port": $port, "user": $user, "password": $pass, "db_name": $db, "ssl_mode": $ssl}}
|
||||
}' "$E2E_API_CONFIG" > "$MERGED_CONFIG"
|
||||
else
|
||||
python3 - "$E2E_API_CONFIG" "$MERGED_CONFIG" << 'PYEOF'
|
||||
import sys, json, os
|
||||
with open(sys.argv[1]) as f: c = json.load(f)
|
||||
pg = {"host": os.environ.get("POSTGRES_HOST", "localhost"), "port": os.environ.get("POSTGRES_PORT", "5432"), "user": os.environ.get("POSTGRES_USER", "bifrost"), "password": os.environ.get("POSTGRES_PASSWORD", "bifrost_password"), "db_name": os.environ.get("POSTGRES_DB", "bifrost"), "ssl_mode": os.environ.get("POSTGRES_SSLMODE", "disable")}
|
||||
c["config_store"] = {"enabled": True, "type": "postgres", "config": pg}
|
||||
c["logs_store"] = {"enabled": True, "type": "postgres", "config": dict(pg)}
|
||||
with open(sys.argv[2], "w") as f: json.dump(c, f, indent=2)
|
||||
PYEOF
|
||||
fi
|
||||
|
||||
echo "🔄 Resetting PostgreSQL database..."
|
||||
DOCKER_COMPOSE_FILE="$REPO_ROOT/.github/workflows/configs/docker-compose.yml"
|
||||
if [ -f "$DOCKER_COMPOSE_FILE" ]; then
|
||||
POSTGRES_CONTAINER=$(docker compose -f "$DOCKER_COMPOSE_FILE" ps -q postgres)
|
||||
if [ -n "$POSTGRES_CONTAINER" ]; then
|
||||
ESCAPED_DB_NAME="${POSTGRES_DB//\"/\"\"}"
|
||||
docker exec "$POSTGRES_CONTAINER" psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d postgres -c "DROP DATABASE IF EXISTS \"$ESCAPED_DB_NAME\";" 2>/dev/null || true
|
||||
docker exec "$POSTGRES_CONTAINER" psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d postgres -c "CREATE DATABASE \"$ESCAPED_DB_NAME\";" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "🚀 Starting Bifrost on port $PORT..."
|
||||
"$BIFROST_BINARY" --app-dir "$TEMP_DIR" --port "$PORT" --log-level debug > "$SERVER_LOG" 2>&1 &
|
||||
BIFROST_PID=$!
|
||||
|
||||
MAX_WAIT=60
|
||||
ELAPSED=0
|
||||
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||
if grep -q "successfully started bifrost" "$SERVER_LOG" 2>/dev/null; then
|
||||
echo " ✅ Bifrost started"
|
||||
break
|
||||
fi
|
||||
if ! kill -0 "$BIFROST_PID" 2>/dev/null; then
|
||||
echo " ❌ Bifrost process exited"
|
||||
cat "$SERVER_LOG"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
ELAPSED=$((ELAPSED + 1))
|
||||
done
|
||||
|
||||
if [ $ELAPSED -ge $MAX_WAIT ]; then
|
||||
echo " ❌ Bifrost did not start within ${MAX_WAIT}s"
|
||||
cat "$SERVER_LOG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export BIFROST_BASE_URL="http://localhost:$PORT"
|
||||
fi
|
||||
|
||||
# ----- Run tests (/v1, /integrations, /api) -----
|
||||
echo ""
|
||||
echo "🧪 Running E2E API tests (Newman)"
|
||||
echo " BIFROST_BASE_URL=$BIFROST_BASE_URL"
|
||||
echo ""
|
||||
|
||||
if ! command -v newman &>/dev/null; then
|
||||
echo "❌ Newman is not installed. Install with: npm install -g newman" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$E2E_API_DIR/setup-plugin.sh" ]; then
|
||||
echo "📦 Setting up test plugin (optional)..."
|
||||
"$E2E_API_DIR/setup-plugin.sh" 2>/dev/null || echo " Plugin setup skipped"
|
||||
fi
|
||||
if [ -f "$E2E_API_DIR/setup-mcp.sh" ]; then
|
||||
echo "🔌 Setting up test MCP server (optional)..."
|
||||
"$E2E_API_DIR/setup-mcp.sh" 2>/dev/null || echo " MCP setup skipped"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
cd "$E2E_API_DIR"
|
||||
|
||||
# In CI (e.g. GitHub Actions), generate HTML reports for artifact upload
|
||||
REPORT_ARGS=""
|
||||
if [ "${GITHUB_ACTIONS:-}" = "true" ] || [ "${CI:-0}" = "1" ]; then
|
||||
REPORT_ARGS="--html"
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
echo "Running /v1 test suite..."
|
||||
echo "=========================================="
|
||||
if ! ./runners/run-newman-inference-tests.sh $REPORT_ARGS; then
|
||||
echo "❌ /v1 test suite failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
echo "Running /integrations test suites..."
|
||||
echo "=========================================="
|
||||
if ! ./runners/run-all-integration-tests.sh $REPORT_ARGS; then
|
||||
echo "❌ /integrations test suites failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
echo "Running /api test suite..."
|
||||
echo "=========================================="
|
||||
if ! ./runners/run-newman-api-tests.sh $REPORT_ARGS; then
|
||||
echo "❌ /api test suite failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
echo "Running inference features test suite..."
|
||||
echo "=========================================="
|
||||
if ! ./runners/run-newman-inference-features-tests.sh $REPORT_ARGS; then
|
||||
echo "❌ inference features test suite failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ All E2E API tests passed (/v1, /integrations, /api, inference features)"
|
||||
141
.github/workflows/scripts/test-e2e-ui.sh
vendored
Executable file
141
.github/workflows/scripts/test-e2e-ui.sh
vendored
Executable file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Test E2E UI with Playwright
|
||||
# Usage: ./test-e2e-ui.sh
|
||||
|
||||
# Setup Go workspace for CI
|
||||
source "$(dirname "$0")/setup-go-workspace.sh"
|
||||
|
||||
echo "🧪 Running E2E UI tests..."
|
||||
|
||||
CONFIGS_DIR=".github/workflows/configs"
|
||||
|
||||
# Cleanup function to ensure all services are stopped
|
||||
cleanup() {
|
||||
echo "🧹 Cleaning up..."
|
||||
if [ -n "${SERVER_PID:-}" ]; then
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
wait "$SERVER_PID" 2>/dev/null || true
|
||||
fi
|
||||
rm -f "${SERVER_LOG:-}"
|
||||
docker compose -f "$CONFIGS_DIR/docker-compose.yml" down 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Register cleanup handler to run on script exit (success or failure)
|
||||
trap cleanup EXIT
|
||||
|
||||
# Build UI
|
||||
echo "🎨 Building UI..."
|
||||
make build-ui
|
||||
|
||||
# Build bifrost-http binary
|
||||
echo "🔨 Building bifrost-http binary..."
|
||||
mkdir -p tmp
|
||||
cd transports/bifrost-http
|
||||
go build -o ../../tmp/bifrost-http .
|
||||
cd ../..
|
||||
|
||||
# Start Docker services
|
||||
echo "🐳 Starting Docker services (PostgreSQL, Redis, etc.)..."
|
||||
docker compose -f "$CONFIGS_DIR/docker-compose.yml" up -d
|
||||
|
||||
# Wait for Docker services to be healthy with polling
|
||||
echo "⏳ Waiting for Docker services to be ready..."
|
||||
MAX_WAIT=300
|
||||
ELAPSED=0
|
||||
SERVICES_READY=false
|
||||
|
||||
# Get expected number of services
|
||||
EXPECTED_SERVICES=$(docker compose -f "$CONFIGS_DIR/docker-compose.yml" config --services 2>/dev/null | wc -l | tr -d ' ')
|
||||
|
||||
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||
# Get running container count
|
||||
RUNNING_COUNT=$(docker compose -f "$CONFIGS_DIR/docker-compose.yml" ps --status running -q 2>/dev/null | wc -l | tr -d ' ')
|
||||
|
||||
# Check health status: count healthy and unhealthy (starting/unhealthy) services
|
||||
HEALTH_OUTPUT=$(docker compose -f "$CONFIGS_DIR/docker-compose.yml" ps --format "{{.Name}}:{{.Health}}" 2>/dev/null)
|
||||
HEALTHY_COUNT=$(echo "$HEALTH_OUTPUT" | grep -c ":healthy") || HEALTHY_COUNT=0
|
||||
UNHEALTHY_COUNT=$(echo "$HEALTH_OUTPUT" | grep -cE ":(starting|unhealthy)") || UNHEALTHY_COUNT=0
|
||||
|
||||
# All services are ready when:
|
||||
# 1. All expected services are running
|
||||
# 2. No services are in "starting" or "unhealthy" state
|
||||
if [ "$RUNNING_COUNT" -eq "$EXPECTED_SERVICES" ] && [ "$UNHEALTHY_COUNT" -eq "0" ]; then
|
||||
SERVICES_READY=true
|
||||
echo "✅ All Docker services are ready ($HEALTHY_COUNT with healthchecks, ${ELAPSED}s)"
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
ELAPSED=$((ELAPSED + 2))
|
||||
echo " ⏳ Waiting for services... ($RUNNING_COUNT/$EXPECTED_SERVICES running, $HEALTHY_COUNT healthy, $UNHEALTHY_COUNT starting, ${ELAPSED}s/${MAX_WAIT}s)"
|
||||
done
|
||||
|
||||
if [ "$SERVICES_READY" = false ]; then
|
||||
echo "❌ Docker services failed to become healthy within ${MAX_WAIT}s"
|
||||
echo " Current service status:"
|
||||
docker compose -f "$CONFIGS_DIR/docker-compose.yml" ps
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Reset PostgreSQL database to clean state
|
||||
echo "🧹 Resetting PostgreSQL database..."
|
||||
docker exec -e PGPASSWORD=bifrost_password "$(docker compose -f "$CONFIGS_DIR/docker-compose.yml" ps -q postgres)" \
|
||||
psql -U bifrost -d postgres \
|
||||
-c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'bifrost' AND pid <> pg_backend_pid();" \
|
||||
-c "DROP DATABASE IF EXISTS bifrost;" \
|
||||
-c "CREATE DATABASE bifrost;"
|
||||
|
||||
# Start bifrost-http server with default config
|
||||
SERVER_LOG=$(mktemp)
|
||||
echo "🚀 Starting bifrost-http server..."
|
||||
./tmp/bifrost-http --app-dir "$CONFIGS_DIR/default" --port 18080 --log-level debug 2>&1 | tee "$SERVER_LOG" &
|
||||
SERVER_PID=$!
|
||||
|
||||
# Wait for server to be ready
|
||||
echo "⏳ Waiting for server to start..."
|
||||
MAX_WAIT=60
|
||||
ELAPSED=0
|
||||
SERVER_READY=false
|
||||
|
||||
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||
if grep -q "successfully started bifrost, serving UI on http://localhost:18080" "$SERVER_LOG" 2>/dev/null; then
|
||||
SERVER_READY=true
|
||||
echo "✅ Server started successfully"
|
||||
break
|
||||
fi
|
||||
|
||||
# Check if server process is still running
|
||||
if ! kill -0 $SERVER_PID 2>/dev/null; then
|
||||
echo "❌ Server process died before starting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
ELAPSED=$((ELAPSED + 1))
|
||||
done
|
||||
|
||||
if [ "$SERVER_READY" = false ]; then
|
||||
echo "❌ Server failed to start within ${MAX_WAIT}s"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install Playwright dependencies
|
||||
echo "📦 Installing Playwright dependencies..."
|
||||
cd tests/e2e
|
||||
npm ci
|
||||
npx playwright install --with-deps chromium
|
||||
|
||||
# Run Playwright tests (BASE_URL = browser; BIFROST_BASE_URL = global-setup API calls).
|
||||
# Forward MCP_SSE_HEADERS so the mcp-registry SSE test can use it (set in workflow env).
|
||||
echo "🎭 Running Playwright E2E tests..."
|
||||
CI=true SKIP_WEB_SERVER=1 BASE_URL=http://localhost:18080 BIFROST_BASE_URL=http://localhost:18080 \
|
||||
MCP_SSE_HEADERS="${MCP_SSE_HEADERS:-}" \
|
||||
npx playwright test --workers=4
|
||||
PLAYWRIGHT_EXIT=$?
|
||||
|
||||
cd ../..
|
||||
|
||||
echo "✅ E2E UI tests completed"
|
||||
exit $PLAYWRIGHT_EXIT
|
||||
61
.github/workflows/scripts/test-framework.sh
vendored
Executable file
61
.github/workflows/scripts/test-framework.sh
vendored
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Test framework component
|
||||
# Usage: ./test-framework.sh
|
||||
|
||||
# Setup Go workspace for CI
|
||||
source "$(dirname "$0")/setup-go-workspace.sh"
|
||||
|
||||
echo "🧪 Running framework tests..."
|
||||
|
||||
# Cleanup function to ensure Docker services are stopped
|
||||
cleanup_docker() {
|
||||
echo "🧹 Cleaning up Docker services..."
|
||||
if command -v docker-compose >/dev/null 2>&1; then
|
||||
docker-compose -f tests/docker-compose.yml down 2>/dev/null || true
|
||||
elif docker compose version >/dev/null 2>&1; then
|
||||
docker compose -f tests/docker-compose.yml down 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Register cleanup handler to run on script exit (success or failure)
|
||||
trap cleanup_docker EXIT
|
||||
|
||||
# Starting dependencies of framework tests
|
||||
echo "🔧 Starting dependencies of framework tests..."
|
||||
# Use docker compose (v2) if available, fallback to docker-compose (v1)
|
||||
if command -v docker-compose >/dev/null 2>&1; then
|
||||
docker-compose -f tests/docker-compose.yml up -d
|
||||
elif docker compose version >/dev/null 2>&1; then
|
||||
docker compose -f tests/docker-compose.yml up -d
|
||||
else
|
||||
echo "❌ Neither docker-compose nor docker compose is available"
|
||||
exit 1
|
||||
fi
|
||||
sleep 20
|
||||
|
||||
# Validate framework build
|
||||
echo "🔨 Validating framework build..."
|
||||
cd framework
|
||||
go build ./...
|
||||
echo "✅ Framework build validation successful"
|
||||
|
||||
# Run framework tests with coverage
|
||||
echo "🧪 Running framework tests with coverage..."
|
||||
go test --race -coverprofile=coverage.txt -coverpkg=./... ./...
|
||||
|
||||
# Upload coverage to Codecov
|
||||
if [ -n "${CODECOV_TOKEN:-}" ]; then
|
||||
echo "📊 Uploading coverage to Codecov..."
|
||||
curl -Os https://uploader.codecov.io/latest/linux/codecov
|
||||
chmod +x codecov
|
||||
./codecov -t "$CODECOV_TOKEN" -f coverage.txt -F framework
|
||||
rm -f codecov coverage.txt
|
||||
else
|
||||
echo "ℹ️ CODECOV_TOKEN not set, skipping coverage upload"
|
||||
rm -f coverage.txt
|
||||
fi
|
||||
cd ..
|
||||
|
||||
echo "✅ Framework tests completed successfully"
|
||||
183
.github/workflows/scripts/test-integrations.sh
vendored
Executable file
183
.github/workflows/scripts/test-integrations.sh
vendored
Executable file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Test integration tests by building bifrost-http from source, starting it,
|
||||
# and running Python and TypeScript SDK integration tests
|
||||
# Usage: ./test-integrations.sh
|
||||
|
||||
# Get the absolute path of the script directory
|
||||
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
else
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
fi
|
||||
|
||||
# Repository root (3 levels up from .github/workflows/scripts)
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd -P)"
|
||||
|
||||
# Setup Go workspace for CI (go.work is gitignored, must be regenerated)
|
||||
source "$SCRIPT_DIR/setup-go-workspace.sh"
|
||||
|
||||
echo "🧪 Running Integration Tests"
|
||||
echo " Repository root: $REPO_ROOT"
|
||||
|
||||
# Configuration
|
||||
TEST_PORT="${PORT:-8080}"
|
||||
TEST_HOST="${HOST:-localhost}"
|
||||
BIFROST_PID=""
|
||||
TEST_FAILED=0
|
||||
LOG_FILE="$(mktemp /tmp/bifrost-integrations.XXXXXX.log)"
|
||||
|
||||
# Cleanup function
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
echo ""
|
||||
echo "🧹 Cleaning up..."
|
||||
|
||||
# Kill Bifrost server if running
|
||||
if [ -n "${BIFROST_PID:-}" ]; then
|
||||
echo " Stopping Bifrost server (PID: $BIFROST_PID)..."
|
||||
kill "$BIFROST_PID" 2>/dev/null || true
|
||||
wait "$BIFROST_PID" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
rm -f "${LOG_FILE:-}" 2>/dev/null || true
|
||||
|
||||
exit $exit_code
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Step 1: Build bifrost-http from source
|
||||
echo ""
|
||||
echo "🔨 Building bifrost-http from source..."
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# Build the UI first, then the binary
|
||||
make build-ui
|
||||
make build
|
||||
|
||||
if [ ! -f "$REPO_ROOT/tmp/bifrost-http" ]; then
|
||||
echo "❌ Error: bifrost-http binary not found at $REPO_ROOT/tmp/bifrost-http"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Build complete: $REPO_ROOT/tmp/bifrost-http"
|
||||
|
||||
# Step 2: Start Bifrost server with Python integration test config
|
||||
echo ""
|
||||
echo "🚀 Starting Bifrost server..."
|
||||
echo " Config: tests/integrations/python/config.json"
|
||||
echo " Host: $TEST_HOST"
|
||||
echo " Port: $TEST_PORT"
|
||||
|
||||
# Start server in background with Python config directory
|
||||
"$REPO_ROOT/tmp/bifrost-http" \
|
||||
-host "$TEST_HOST" \
|
||||
-port "$TEST_PORT" \
|
||||
-log-style json \
|
||||
-log-level info \
|
||||
-app-dir "$REPO_ROOT/tests/integrations/python" \
|
||||
> "$LOG_FILE" 2>&1 &
|
||||
|
||||
BIFROST_PID=$!
|
||||
echo " Started with PID: $BIFROST_PID"
|
||||
|
||||
|
||||
# Wait for server to be ready
|
||||
echo "⏳ Waiting for Bifrost to be ready..."
|
||||
MAX_WAIT=30
|
||||
ELAPSED=0
|
||||
SERVER_READY=false
|
||||
|
||||
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||
if curl --connect-timeout 10 --max-time 20 -sf "http://$TEST_HOST:$TEST_PORT/health" > /dev/null 2>&1; then
|
||||
SERVER_READY=true
|
||||
echo "✅ Bifrost is ready (took ${ELAPSED}s)"
|
||||
break
|
||||
fi
|
||||
|
||||
# Check if server process is still running
|
||||
if ! kill -0 "$BIFROST_PID" 2>/dev/null; then
|
||||
echo "❌ Bifrost process died unexpectedly"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
ELAPSED=$((ELAPSED + 1))
|
||||
done
|
||||
|
||||
if [ "$SERVER_READY" = false ]; then
|
||||
echo "❌ Bifrost failed to start within ${MAX_WAIT}s"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set environment variable for tests
|
||||
export BIFROST_BASE_URL="http://$TEST_HOST:$TEST_PORT"
|
||||
echo " BIFROST_BASE_URL=$BIFROST_BASE_URL"
|
||||
|
||||
# Step 3: Run Python integration tests
|
||||
echo ""
|
||||
echo "🐍 Running Python integration tests..."
|
||||
echo "="
|
||||
|
||||
cd "$REPO_ROOT/tests/integrations/python"
|
||||
|
||||
# Check if uv is available
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
echo "📦 Installing Python dependencies with uv..."
|
||||
uv sync --frozen --quiet
|
||||
|
||||
echo ""
|
||||
echo "🏃 Running Python tests..."
|
||||
if ! uv run pytest -v --tb=short; then
|
||||
echo "⚠️ Python tests failed"
|
||||
TEST_FAILED=1
|
||||
fi
|
||||
else
|
||||
echo "⚠️ uv not found, trying pip..."
|
||||
|
||||
# Create virtual environment if needed
|
||||
if [ ! -d ".venv" ]; then
|
||||
python3 -m venv .venv
|
||||
fi
|
||||
|
||||
source .venv/bin/activate
|
||||
pip install -q -e .
|
||||
|
||||
echo ""
|
||||
echo "🏃 Running Python tests..."
|
||||
if ! pytest -v --tb=short; then
|
||||
echo "⚠️ Python tests failed"
|
||||
TEST_FAILED=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 4: Run TypeScript integration tests
|
||||
echo ""
|
||||
echo "📘 Running TypeScript integration tests..."
|
||||
echo "="
|
||||
|
||||
cd "$REPO_ROOT/tests/integrations/typescript"
|
||||
|
||||
# Install dependencies if needed
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "📦 Installing TypeScript dependencies with npm..."
|
||||
npm ci
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🏃 Running TypeScript tests..."
|
||||
if ! npm test; then
|
||||
echo "⚠️ TypeScript tests failed"
|
||||
TEST_FAILED=1
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "="
|
||||
if [ $TEST_FAILED -eq 1 ]; then
|
||||
echo "❌ Some integration tests failed"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ All integration tests passed!"
|
||||
fi
|
||||
85
.github/workflows/scripts/upload-cli-to-r2.sh
vendored
Executable file
85
.github/workflows/scripts/upload-cli-to-r2.sh
vendored
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Upload CLI builds to R2 with retry logic
|
||||
# Usage: ./upload-cli-to-r2.sh <cli-version>
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 <cli-version> (e.g., cli/v1.0.0)"
|
||||
exit 1
|
||||
fi
|
||||
CLI_TAG="$1"
|
||||
# Validate tag format: must be cli/vX.Y.Z or cli/vX.Y.Z-prerelease
|
||||
if [[ ! "$CLI_TAG" =~ ^cli/v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
|
||||
echo "❌ Invalid tag format: $CLI_TAG"
|
||||
echo " Expected format: cli/vX.Y.Z or cli/vX.Y.Z-prerelease (e.g., cli/v1.0.0, cli/v1.0.0-rc.1)"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "./dist" ]]; then
|
||||
echo "❌ ./dist not found. Build artifacts must be present before upload."
|
||||
exit 1
|
||||
fi
|
||||
: "${R2_ENDPOINT:?R2_ENDPOINT env var is required}"
|
||||
: "${R2_BUCKET:?R2_BUCKET env var is required}"
|
||||
|
||||
# Strip 'cli/' prefix from version
|
||||
VERSION_ONLY=${CLI_TAG#cli/v}
|
||||
CLI_VERSION="v${VERSION_ONLY}"
|
||||
printf '%s' "$CLI_VERSION" > "./dist/version.txt"
|
||||
R2_ENDPOINT="$(echo "$R2_ENDPOINT" | tr -d '[:space:]')"
|
||||
|
||||
echo "📤 Uploading CLI binaries for version: $CLI_VERSION"
|
||||
|
||||
# Function to upload with retry
|
||||
upload_with_retry() {
|
||||
local source_path="$1"
|
||||
local dest_path="$2"
|
||||
local max_retries=3
|
||||
|
||||
for attempt in $(seq 1 $max_retries); do
|
||||
echo "🔄 Attempt $attempt/$max_retries: Uploading to $dest_path"
|
||||
|
||||
if aws s3 sync "$source_path" "$dest_path" \
|
||||
--endpoint-url "$R2_ENDPOINT" \
|
||||
--profile "${R2_AWS_PROFILE:-R2}" \
|
||||
--no-progress \
|
||||
--delete; then
|
||||
echo "✅ Upload successful to $dest_path"
|
||||
return 0
|
||||
else
|
||||
echo "⚠️ Attempt $attempt failed"
|
||||
if [ $attempt -lt $max_retries ]; then
|
||||
delay=$((2 ** attempt))
|
||||
echo "🕐 Waiting ${delay}s before retry..."
|
||||
sleep $delay
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "❌ All $max_retries attempts failed for $dest_path"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Upload to versioned path
|
||||
if ! upload_with_retry "./dist/" "s3://$R2_BUCKET/bifrost-cli/$CLI_VERSION/"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if this is a prerelease version (semver: presence of a hyphen denotes pre-release)
|
||||
if [[ "$CLI_VERSION" == *-* ]]; then
|
||||
echo "🔍 Detected prerelease version: $CLI_VERSION"
|
||||
echo "⏭️ Skipping upload to latest/ for prerelease"
|
||||
else
|
||||
echo "🔍 Detected stable release: $CLI_VERSION"
|
||||
|
||||
# Small delay between uploads (configurable; default 2s)
|
||||
sleep "${INTER_UPLOAD_SLEEP_SECONDS:-2}"
|
||||
|
||||
# Upload to latest path
|
||||
echo "📤 Uploading to latest/"
|
||||
if ! upload_with_retry "./dist/" "s3://$R2_BUCKET/bifrost-cli/latest/"; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "🎉 All CLI binaries uploaded successfully to R2"
|
||||
95
.github/workflows/scripts/upload-to-r2.sh
vendored
Executable file
95
.github/workflows/scripts/upload-to-r2.sh
vendored
Executable file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Upload builds to R2 with retry logic
|
||||
# Usage: ./upload-to-r2.sh <transport-version>
|
||||
#
|
||||
# Environment variables:
|
||||
# R2_ENDPOINT - Required. R2 endpoint URL
|
||||
# R2_BUCKET - Required. R2 bucket name
|
||||
# R2_AWS_PROFILE - Optional. AWS CLI profile (default: R2)
|
||||
# SKIP_LATEST_UPLOAD - Optional. Set to "true" to skip latest/ upload
|
||||
# (used when multiple jobs upload in parallel and
|
||||
# the finalize job handles latest/ separately)
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 <transport-version> (e.g., transports/v1.2.3)"
|
||||
exit 1
|
||||
fi
|
||||
TRANSPORT_VERSION="$1"
|
||||
if [[ ! -d "./dist" ]]; then
|
||||
echo "❌ ./dist not found. Build artifacts must be present before upload."
|
||||
exit 1
|
||||
fi
|
||||
: "${R2_ENDPOINT:?R2_ENDPOINT env var is required}"
|
||||
: "${R2_BUCKET:?R2_BUCKET env var is required}"
|
||||
|
||||
# Strip 'transports/' prefix from version
|
||||
VERSION_ONLY=${TRANSPORT_VERSION#transports/v}
|
||||
CLI_VERSION="v${VERSION_ONLY}"
|
||||
R2_ENDPOINT="$(echo "$R2_ENDPOINT" | tr -d '[:space:]')"
|
||||
|
||||
echo "📤 Uploading binaries for version: $CLI_VERSION"
|
||||
|
||||
# Function to upload with retry
|
||||
# Uses aws s3 cp --recursive instead of s3 sync --delete so that
|
||||
# parallel upload jobs don't wipe each other's files.
|
||||
upload_with_retry() {
|
||||
local source_path="$1"
|
||||
local dest_path="$2"
|
||||
local max_retries=3
|
||||
|
||||
for attempt in $(seq 1 $max_retries); do
|
||||
echo "🔄 Attempt $attempt/$max_retries: Uploading to $dest_path"
|
||||
|
||||
if aws s3 cp "$source_path" "$dest_path" \
|
||||
--endpoint-url "$R2_ENDPOINT" \
|
||||
--profile "${R2_AWS_PROFILE:-R2}" \
|
||||
--no-progress \
|
||||
--recursive; then
|
||||
echo "✅ Upload successful to $dest_path"
|
||||
return 0
|
||||
else
|
||||
echo "⚠️ Attempt $attempt failed"
|
||||
if [ $attempt -lt $max_retries ]; then
|
||||
delay=$((2 ** attempt))
|
||||
echo "🕐 Waiting ${delay}s before retry..."
|
||||
sleep $delay
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "❌ All $max_retries attempts failed for $dest_path"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Upload to versioned path
|
||||
if ! upload_with_retry "./dist/" "s3://$R2_BUCKET/bifrost/$CLI_VERSION/"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Skip latest/ upload if requested (finalize job handles it after all builds complete)
|
||||
if [[ "${SKIP_LATEST_UPLOAD:-false}" == "true" ]]; then
|
||||
echo "⏭️ Skipping latest/ upload (will be handled by finalize job)"
|
||||
echo "🎉 Binaries uploaded successfully to R2 (versioned path)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if this is a prerelease version (semver: presence of a hyphen denotes pre-release)
|
||||
if [[ "$CLI_VERSION" == *-* ]]; then
|
||||
echo "🔍 Detected prerelease version: $CLI_VERSION"
|
||||
echo "⏭️ Skipping upload to latest/ for prerelease"
|
||||
else
|
||||
echo "🔍 Detected stable release: $CLI_VERSION"
|
||||
|
||||
# Small delay between uploads (configurable; default 2s)
|
||||
sleep "${INTER_UPLOAD_SLEEP_SECONDS:-2}"
|
||||
|
||||
# Upload to latest path
|
||||
echo "📤 Uploading to latest/"
|
||||
if ! upload_with_retry "./dist/" "s3://$R2_BUCKET/bifrost/latest/"; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "🎉 All binaries uploaded successfully to R2"
|
||||
201
.github/workflows/scripts/validate-go-config-fields.sh
vendored
Executable file
201
.github/workflows/scripts/validate-go-config-fields.sh
vendored
Executable file
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Validate that config.schema.json stays in sync with Go struct JSON tags
|
||||
# Extracts json:"..." tags from Go structs and compares against schema properties
|
||||
|
||||
echo "🔍 Validating Go struct fields vs config.schema.json..."
|
||||
echo "========================================================"
|
||||
|
||||
# Get the repository root
|
||||
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
else
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
fi
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
|
||||
CONFIG_SCHEMA="$REPO_ROOT/transports/config.schema.json"
|
||||
|
||||
# Color codes
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
ERRORS=0
|
||||
WARNINGS=0
|
||||
|
||||
# Check prerequisites
|
||||
if [ ! -f "$CONFIG_SCHEMA" ]; then
|
||||
echo "❌ Config schema not found: $CONFIG_SCHEMA"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "❌ jq is required for Go-to-schema validation"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract JSON tags from a Go struct
|
||||
# Usage: extract_go_json_tags <file> <struct_name>
|
||||
# Returns sorted list of json tag names (excluding "-" and ",omitempty" suffixes)
|
||||
extract_go_json_tags() {
|
||||
local file=$1
|
||||
local struct_name=$2
|
||||
awk "/^type ${struct_name} struct/,/^}/" "$file" \
|
||||
| grep -oE 'json:"([^"]+)"' \
|
||||
| sed 's/json:"//;s/"//' \
|
||||
| sed 's/,.*//' \
|
||||
| grep -v '^-$' \
|
||||
| sort
|
||||
}
|
||||
|
||||
# Extract property keys from config.schema.json at a given jq path
|
||||
# Usage: extract_schema_keys <jq_path>
|
||||
extract_schema_keys() {
|
||||
local jq_path=$1
|
||||
jq -r "${jq_path} | keys[]" "$CONFIG_SCHEMA" 2>/dev/null | sort
|
||||
}
|
||||
|
||||
# Compare Go struct tags against schema properties
|
||||
# Usage: compare_struct_to_schema <label> <go_file> <struct_name> <jq_path> <exclusions...>
|
||||
compare_struct_to_schema() {
|
||||
local label=$1
|
||||
local go_file=$2
|
||||
local struct_name=$3
|
||||
local jq_path=$4
|
||||
shift 4
|
||||
local exclusions=("$@")
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN} Checking: $label ($struct_name)${NC}"
|
||||
|
||||
if [ ! -f "$go_file" ]; then
|
||||
echo -e "${RED} ❌ Go file not found: $go_file${NC}"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
local go_tags
|
||||
go_tags=$(extract_go_json_tags "$go_file" "$struct_name")
|
||||
|
||||
local schema_keys
|
||||
schema_keys=$(extract_schema_keys "$jq_path")
|
||||
|
||||
if [ -z "$go_tags" ]; then
|
||||
echo -e "${RED} ❌ No JSON tags found for struct $struct_name in $go_file${NC}"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -z "$schema_keys" ]; then
|
||||
echo -e "${RED} ❌ No properties found at $jq_path in config.schema.json${NC}"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
local has_error=false
|
||||
|
||||
# Check Go fields missing from schema
|
||||
while IFS= read -r tag; do
|
||||
[ -z "$tag" ] && continue
|
||||
# Check if excluded
|
||||
local excluded=false
|
||||
for exc in "${exclusions[@]+"${exclusions[@]}"}"; do
|
||||
if [ "$tag" = "$exc" ]; then
|
||||
excluded=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "$excluded" = "true" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! echo "$schema_keys" | grep -qx "$tag"; then
|
||||
echo -e "${RED} ❌ Go field '$tag' missing from schema ($jq_path)${NC}"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
has_error=true
|
||||
fi
|
||||
done <<< "$go_tags"
|
||||
|
||||
# Check schema fields missing from Go (warnings only)
|
||||
while IFS= read -r key; do
|
||||
[ -z "$key" ] && continue
|
||||
if ! echo "$go_tags" | grep -qx "$key"; then
|
||||
echo -e "${YELLOW} ⚠️ Schema property '$key' not found in Go struct $struct_name${NC}"
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
fi
|
||||
done <<< "$schema_keys"
|
||||
|
||||
if [ "$has_error" = "false" ]; then
|
||||
echo -e "${GREEN} ✅ All Go fields present in schema${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "🔍 Comparing Go struct JSON tags against config.schema.json properties..."
|
||||
|
||||
# ClientConfig — framework/configstore/clientconfig.go → .properties.client.properties
|
||||
compare_struct_to_schema \
|
||||
"Client Config" \
|
||||
"$REPO_ROOT/framework/configstore/clientconfig.go" \
|
||||
"ClientConfig" \
|
||||
'.properties.client.properties'
|
||||
|
||||
# GovernanceConfig — framework/configstore/clientconfig.go → .properties.governance.properties
|
||||
compare_struct_to_schema \
|
||||
"Governance Config" \
|
||||
"$REPO_ROOT/framework/configstore/clientconfig.go" \
|
||||
"GovernanceConfig" \
|
||||
'.properties.governance.properties'
|
||||
|
||||
# MCPConfig — core/schemas/mcp.go → .properties.mcp.properties
|
||||
compare_struct_to_schema \
|
||||
"MCP Config" \
|
||||
"$REPO_ROOT/core/schemas/mcp.go" \
|
||||
"MCPConfig" \
|
||||
'.properties.mcp.properties'
|
||||
|
||||
# MCPToolManagerConfig — core/schemas/mcp.go → .$defs.mcp_tool_manager_config.properties
|
||||
compare_struct_to_schema \
|
||||
"MCP Tool Manager Config" \
|
||||
"$REPO_ROOT/core/schemas/mcp.go" \
|
||||
"MCPToolManagerConfig" \
|
||||
'."$defs".mcp_tool_manager_config.properties'
|
||||
|
||||
# MCPClientConfig — core/schemas/mcp.go → .$defs.mcp_client_config.properties
|
||||
# Exclude: state (runtime-only), config_hash (internal)
|
||||
compare_struct_to_schema \
|
||||
"MCP Client Config" \
|
||||
"$REPO_ROOT/core/schemas/mcp.go" \
|
||||
"MCPClientConfig" \
|
||||
'."$defs".mcp_client_config.properties' \
|
||||
"state" \
|
||||
"config_hash"
|
||||
|
||||
# PluginConfig — core/schemas/plugin.go → .properties.plugins.items.properties
|
||||
compare_struct_to_schema \
|
||||
"Plugin Config" \
|
||||
"$REPO_ROOT/core/schemas/plugin.go" \
|
||||
"PluginConfig" \
|
||||
'.properties.plugins.items.properties'
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "========================================================"
|
||||
echo "🏁 Go-to-Schema Validation Complete!"
|
||||
echo "========================================================"
|
||||
echo -e "${GREEN}Errors: $ERRORS${NC}"
|
||||
echo -e "${YELLOW}Warnings: $WARNINGS${NC}"
|
||||
echo ""
|
||||
|
||||
if [ "$ERRORS" -gt 0 ]; then
|
||||
echo -e "${RED}❌ Some Go struct fields are missing from config.schema.json.${NC}"
|
||||
echo " Add the missing properties to transports/config.schema.json"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}✅ All Go struct fields are present in config.schema.json!${NC}"
|
||||
exit 0
|
||||
fi
|
||||
1300
.github/workflows/scripts/validate-helm-config-fields.sh
vendored
Executable file
1300
.github/workflows/scripts/validate-helm-config-fields.sh
vendored
Executable file
File diff suppressed because it is too large
Load Diff
664
.github/workflows/scripts/validate-helm-schema.sh
vendored
Executable file
664
.github/workflows/scripts/validate-helm-schema.sh
vendored
Executable file
@@ -0,0 +1,664 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Validate that the Helm chart values.schema.json is in sync with config.schema.json
|
||||
# This script extracts required fields from both schemas and compares them
|
||||
|
||||
# Get the repository root
|
||||
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
else
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
fi
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
|
||||
CONFIG_SCHEMA="$REPO_ROOT/transports/config.schema.json"
|
||||
HELM_SCHEMA="$REPO_ROOT/helm-charts/bifrost/values.schema.json"
|
||||
|
||||
echo "📋 Comparing schemas:"
|
||||
echo " Config schema: $CONFIG_SCHEMA"
|
||||
echo " Helm schema: $HELM_SCHEMA"
|
||||
|
||||
# Check if files exist
|
||||
if [ ! -f "$CONFIG_SCHEMA" ]; then
|
||||
echo "❌ Config schema not found: $CONFIG_SCHEMA"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$HELM_SCHEMA" ]; then
|
||||
echo "❌ Helm schema not found: $HELM_SCHEMA"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if jq is available
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "⚠️ jq not found, skipping detailed schema comparison"
|
||||
echo " Install jq for full schema validation"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ERRORS=0
|
||||
|
||||
# Function to extract required fields from a schema definition
|
||||
extract_required_fields() {
|
||||
local schema_file="$1"
|
||||
local def_path="$2"
|
||||
jq -r "$def_path.required // [] | .[]" "$schema_file" 2>/dev/null | sort
|
||||
}
|
||||
|
||||
# Function to check if a definition exists in schema
|
||||
def_exists() {
|
||||
local schema_file="$1"
|
||||
local def_path="$2"
|
||||
jq -e "$def_path" "$schema_file" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "🔍 Checking required fields in governance entities..."
|
||||
|
||||
# Check governance.budgets required fields
|
||||
CONFIG_BUDGET_REQUIRED=$(jq -r '.properties.governance.properties.budgets.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_BUDGET_REQUIRED=$(jq -r '.properties.bifrost.properties.governance.properties.budgets.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_BUDGET_REQUIRED" != "$HELM_BUDGET_REQUIRED" ]; then
|
||||
echo "❌ Budget required fields mismatch:"
|
||||
echo " Config: [$CONFIG_BUDGET_REQUIRED]"
|
||||
echo " Helm: [$HELM_BUDGET_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Budget required fields match: [$CONFIG_BUDGET_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check governance.rate_limits required fields
|
||||
CONFIG_RATELIMIT_REQUIRED=$(jq -r '.properties.governance.properties.rate_limits.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_RATELIMIT_REQUIRED=$(jq -r '.properties.bifrost.properties.governance.properties.rateLimits.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_RATELIMIT_REQUIRED" != "$HELM_RATELIMIT_REQUIRED" ]; then
|
||||
echo "❌ Rate limits required fields mismatch:"
|
||||
echo " Config: [$CONFIG_RATELIMIT_REQUIRED]"
|
||||
echo " Helm: [$HELM_RATELIMIT_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Rate limits required fields match: [$CONFIG_RATELIMIT_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check governance.customers required fields
|
||||
CONFIG_CUSTOMER_REQUIRED=$(jq -r '.properties.governance.properties.customers.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_CUSTOMER_REQUIRED=$(jq -r '.properties.bifrost.properties.governance.properties.customers.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_CUSTOMER_REQUIRED" != "$HELM_CUSTOMER_REQUIRED" ]; then
|
||||
echo "❌ Customer required fields mismatch:"
|
||||
echo " Config: [$CONFIG_CUSTOMER_REQUIRED]"
|
||||
echo " Helm: [$HELM_CUSTOMER_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Customer required fields match: [$CONFIG_CUSTOMER_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check governance.teams required fields
|
||||
CONFIG_TEAM_REQUIRED=$(jq -r '.properties.governance.properties.teams.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_TEAM_REQUIRED=$(jq -r '.properties.bifrost.properties.governance.properties.teams.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_TEAM_REQUIRED" != "$HELM_TEAM_REQUIRED" ]; then
|
||||
echo "❌ Team required fields mismatch:"
|
||||
echo " Config: [$CONFIG_TEAM_REQUIRED]"
|
||||
echo " Helm: [$HELM_TEAM_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Team required fields match: [$CONFIG_TEAM_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check governance.virtual_keys required fields
|
||||
CONFIG_VK_REQUIRED=$(jq -r '.properties.governance.properties.virtual_keys.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_VK_REQUIRED=$(jq -r '.properties.bifrost.properties.governance.properties.virtualKeys.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_VK_REQUIRED" != "$HELM_VK_REQUIRED" ]; then
|
||||
echo "❌ Virtual key required fields mismatch:"
|
||||
echo " Config: [$CONFIG_VK_REQUIRED]"
|
||||
echo " Helm: [$HELM_VK_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Virtual key required fields match: [$CONFIG_VK_REQUIRED]"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo '🔍 Checking required fields in $defs...'
|
||||
|
||||
# Check base_key required fields
|
||||
CONFIG_BASEKEY_REQUIRED=$(jq -r '."$defs".base_key.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_BASEKEY_REQUIRED=$(jq -r '."$defs".providerKey.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_BASEKEY_REQUIRED" != "$HELM_BASEKEY_REQUIRED" ]; then
|
||||
echo "❌ Provider key (base_key) required fields mismatch:"
|
||||
echo " Config: [$CONFIG_BASEKEY_REQUIRED]"
|
||||
echo " Helm: [$HELM_BASEKEY_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Provider key required fields match: [$CONFIG_BASEKEY_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check azure_key_config required fields
|
||||
CONFIG_AZURE_REQUIRED=$(jq -r '."$defs".azure_key.allOf[1].properties.azure_key_config.properties | keys | map(select(. as $k | ["endpoint", "api_version"] | index($k))) | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "endpoint,api_version")
|
||||
HELM_AZURE_REQUIRED=$(jq -r '."$defs".providerKey.properties.azure_key_config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
# Normalize the comparison (config schema uses allOf pattern)
|
||||
CONFIG_AZURE_REQ_NORM=$(jq -r '."$defs".azure_key.allOf[1].properties.azure_key_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
if [ -z "$CONFIG_AZURE_REQ_NORM" ]; then
|
||||
# Try the direct path in $defs
|
||||
CONFIG_AZURE_REQ_NORM=$(jq -r '."$defs".azure_key_config.required // ["endpoint", "api_version"] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "api_version,endpoint")
|
||||
fi
|
||||
|
||||
if [ "$CONFIG_AZURE_REQ_NORM" != "$HELM_AZURE_REQUIRED" ]; then
|
||||
echo "❌ Azure key config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_AZURE_REQ_NORM]"
|
||||
echo " Helm: [$HELM_AZURE_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Azure key config required fields match: [$HELM_AZURE_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check vertex_key_config required fields
|
||||
CONFIG_VERTEX_REQUIRED=$(jq -r '."$defs".vertex_key.allOf[1].properties.vertex_key_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_VERTEX_REQUIRED=$(jq -r '."$defs".providerKey.properties.vertex_key_config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_VERTEX_REQUIRED" != "$HELM_VERTEX_REQUIRED" ]; then
|
||||
echo "❌ Vertex key config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_VERTEX_REQUIRED]"
|
||||
echo " Helm: [$HELM_VERTEX_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Vertex key config required fields match: [$CONFIG_VERTEX_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check bedrock_key_config required fields
|
||||
CONFIG_BEDROCK_REQUIRED=$(jq -r '."$defs".bedrock_key.allOf[1].properties.bedrock_key_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_BEDROCK_REQUIRED=$(jq -r '."$defs".providerKey.properties.bedrock_key_config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_BEDROCK_REQUIRED" != "$HELM_BEDROCK_REQUIRED" ]; then
|
||||
echo "❌ Bedrock key config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_BEDROCK_REQUIRED]"
|
||||
echo " Helm: [$HELM_BEDROCK_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Bedrock key config required fields match: [$CONFIG_BEDROCK_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check vllm_key_config required fields
|
||||
CONFIG_VLLM_REQUIRED=$(jq -r '."$defs".vllm_key.allOf[1].properties.vllm_key_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_VLLM_REQUIRED=$(jq -r '."$defs".providerKey.properties.vllm_key_config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_VLLM_REQUIRED" != "$HELM_VLLM_REQUIRED" ]; then
|
||||
echo "❌ VLLM key config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_VLLM_REQUIRED]"
|
||||
echo " Helm: [$HELM_VLLM_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ VLLM key config required fields match: [$HELM_VLLM_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check concurrency_and_buffer_size required fields (renamed from concurrency_config)
|
||||
CONFIG_CONCURRENCY_REQUIRED=$(jq -r '."$defs".concurrency_and_buffer_size.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_CONCURRENCY_REQUIRED=$(jq -r '."$defs".concurrencyConfig.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_CONCURRENCY_REQUIRED" != "$HELM_CONCURRENCY_REQUIRED" ]; then
|
||||
echo "❌ Concurrency config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_CONCURRENCY_REQUIRED]"
|
||||
echo " Helm: [$HELM_CONCURRENCY_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Concurrency config required fields match: [$CONFIG_CONCURRENCY_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check proxy_config required fields
|
||||
CONFIG_PROXY_REQUIRED=$(jq -r '."$defs".proxy_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_PROXY_REQUIRED=$(jq -r '."$defs".proxyConfig.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_PROXY_REQUIRED" != "$HELM_PROXY_REQUIRED" ]; then
|
||||
echo "❌ Proxy config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_PROXY_REQUIRED]"
|
||||
echo " Helm: [$HELM_PROXY_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Proxy config required fields match: [$CONFIG_PROXY_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check mcp_client_config required fields
|
||||
# Note: Config uses snake_case (connection_type), Helm uses camelCase (connectionType)
|
||||
CONFIG_MCP_REQUIRED=$(jq -r '."$defs".mcp_client_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_MCP_REQUIRED=$(jq -r '."$defs".mcpClientConfig.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
# Normalize config snake_case to camelCase for comparison
|
||||
CONFIG_MCP_NORM=$(echo "$CONFIG_MCP_REQUIRED" | tr ',' '\n' | sed 's/connection_type/connectionType/' | sort | tr '\n' ',' | sed 's/,$//')
|
||||
|
||||
if [ "$CONFIG_MCP_NORM" != "$HELM_MCP_REQUIRED" ]; then
|
||||
echo "❌ MCP client config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_MCP_REQUIRED] (normalized: [$CONFIG_MCP_NORM])"
|
||||
echo " Helm: [$HELM_MCP_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ MCP client config required fields match: [$HELM_MCP_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check provider $def required fields
|
||||
CONFIG_PROVIDER_REQUIRED=$(jq -r '."$defs".provider.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_PROVIDER_REQUIRED=$(jq -r '."$defs".provider.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_PROVIDER_REQUIRED" != "$HELM_PROVIDER_REQUIRED" ]; then
|
||||
echo "❌ Provider def required fields mismatch:"
|
||||
echo " Config: [$CONFIG_PROVIDER_REQUIRED]"
|
||||
echo " Helm: [$HELM_PROVIDER_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Provider def required fields match: [$CONFIG_PROVIDER_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check routing_rule required fields
|
||||
CONFIG_ROUTING_REQUIRED=$(jq -r '."$defs".routing_rule.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_ROUTING_REQUIRED=$(jq -r '.properties.bifrost.properties.governance.properties.routingRules.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_ROUTING_REQUIRED" != "$HELM_ROUTING_REQUIRED" ]; then
|
||||
echo "❌ Routing rule required fields mismatch:"
|
||||
echo " Config: [$CONFIG_ROUTING_REQUIRED]"
|
||||
echo " Helm: [$HELM_ROUTING_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Routing rule required fields match: [$CONFIG_ROUTING_REQUIRED]"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔍 Checking required fields in guardrails..."
|
||||
|
||||
# Check guardrail_rules required fields
|
||||
CONFIG_GUARDRAIL_RULE_REQUIRED=$(jq -r '.properties.guardrails_config.properties.guardrail_rules.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
# Also check in $defs
|
||||
if [ -z "$CONFIG_GUARDRAIL_RULE_REQUIRED" ]; then
|
||||
CONFIG_GUARDRAIL_RULE_REQUIRED=$(jq -r '."$defs".guardrails_config.properties.guardrail_rules.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
fi
|
||||
HELM_GUARDRAIL_RULE_REQUIRED=$(jq -r '.properties.bifrost.properties.guardrails.properties.rules.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_GUARDRAIL_RULE_REQUIRED" != "$HELM_GUARDRAIL_RULE_REQUIRED" ]; then
|
||||
echo "❌ Guardrail rules required fields mismatch:"
|
||||
echo " Config: [$CONFIG_GUARDRAIL_RULE_REQUIRED]"
|
||||
echo " Helm: [$HELM_GUARDRAIL_RULE_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Guardrail rules required fields match: [$CONFIG_GUARDRAIL_RULE_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check guardrail_providers required fields
|
||||
CONFIG_GUARDRAIL_PROV_REQUIRED=$(jq -r '.properties.guardrails_config.properties.guardrail_providers.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
if [ -z "$CONFIG_GUARDRAIL_PROV_REQUIRED" ]; then
|
||||
CONFIG_GUARDRAIL_PROV_REQUIRED=$(jq -r '."$defs".guardrails_config.properties.guardrail_providers.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
fi
|
||||
HELM_GUARDRAIL_PROV_REQUIRED=$(jq -r '.properties.bifrost.properties.guardrails.properties.providers.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_GUARDRAIL_PROV_REQUIRED" != "$HELM_GUARDRAIL_PROV_REQUIRED" ]; then
|
||||
echo "❌ Guardrail providers required fields mismatch:"
|
||||
echo " Config: [$CONFIG_GUARDRAIL_PROV_REQUIRED]"
|
||||
echo " Helm: [$HELM_GUARDRAIL_PROV_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Guardrail providers required fields match: [$CONFIG_GUARDRAIL_PROV_REQUIRED]"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔍 Checking required fields in cluster config..."
|
||||
|
||||
# Check cluster gossip required fields (port, config)
|
||||
CONFIG_GOSSIP_TOP_REQUIRED=$(jq -r '."$defs".cluster_config.properties.gossip.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_GOSSIP_TOP_REQUIRED=$(jq -r '.properties.bifrost.properties.cluster.properties.gossip.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_GOSSIP_TOP_REQUIRED" != "$HELM_GOSSIP_TOP_REQUIRED" ]; then
|
||||
echo "❌ Cluster gossip required fields mismatch:"
|
||||
echo " Config: [$CONFIG_GOSSIP_TOP_REQUIRED]"
|
||||
echo " Helm: [$HELM_GOSSIP_TOP_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Cluster gossip required fields match: [$CONFIG_GOSSIP_TOP_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check cluster gossip config required fields (timeout_seconds, success_threshold, failure_threshold)
|
||||
CONFIG_GOSSIP_REQUIRED=$(jq -r '."$defs".cluster_config.properties.gossip.properties.config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_GOSSIP_REQUIRED=$(jq -r '.properties.bifrost.properties.cluster.properties.gossip.properties.config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
# Normalize field names (config uses snake_case, helm uses camelCase)
|
||||
CONFIG_GOSSIP_NORM=$(echo "$CONFIG_GOSSIP_REQUIRED" | tr ',' '\n' | sed 's/failure_threshold/failureThreshold/;s/success_threshold/successThreshold/;s/timeout_seconds/timeoutSeconds/' | sort | tr '\n' ',' | sed 's/,$//')
|
||||
|
||||
if [ "$CONFIG_GOSSIP_NORM" != "$HELM_GOSSIP_REQUIRED" ]; then
|
||||
echo "❌ Cluster gossip config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_GOSSIP_REQUIRED] (normalized: [$CONFIG_GOSSIP_NORM])"
|
||||
echo " Helm: [$HELM_GOSSIP_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Cluster gossip config required fields match: [$HELM_GOSSIP_REQUIRED]"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔍 Checking required fields in virtual_key_provider_config..."
|
||||
|
||||
# Check virtual_key_provider_config required fields
|
||||
CONFIG_VKPC_REQUIRED=$(jq -r '."$defs".virtual_key_provider_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_VKPC_REQUIRED=$(jq -r '."$defs".virtualKeyProviderConfig.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_VKPC_REQUIRED" != "$HELM_VKPC_REQUIRED" ]; then
|
||||
echo "❌ Virtual key provider config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_VKPC_REQUIRED]"
|
||||
echo " Helm: [$HELM_VKPC_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Virtual key provider config required fields match: [$CONFIG_VKPC_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check virtual_key_provider_config keys items required fields (key_id, name, value)
|
||||
CONFIG_VKPC_KEY_REQUIRED=$(jq -r '."$defs".virtual_key_provider_config.properties.keys.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_VKPC_KEY_REQUIRED=$(jq -r '."$defs".virtualKeyProviderConfig.properties.keys.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_VKPC_KEY_REQUIRED" != "$HELM_VKPC_KEY_REQUIRED" ]; then
|
||||
echo "❌ VK provider config key items required fields mismatch:"
|
||||
echo " Config: [$CONFIG_VKPC_KEY_REQUIRED]"
|
||||
echo " Helm: [$HELM_VKPC_KEY_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ VK provider config key items required fields match: [$CONFIG_VKPC_KEY_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check VK provider config key azure_key_config required fields
|
||||
CONFIG_VKPC_AZURE_REQUIRED=$(jq -r '."$defs".virtual_key_provider_config.properties.keys.items.properties.azure_key_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_VKPC_AZURE_REQUIRED=$(jq -r '."$defs".virtualKeyProviderConfig.properties.keys.items.properties.azure_key_config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_VKPC_AZURE_REQUIRED" != "$HELM_VKPC_AZURE_REQUIRED" ]; then
|
||||
echo "❌ VK provider config key azure_key_config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_VKPC_AZURE_REQUIRED]"
|
||||
echo " Helm: [$HELM_VKPC_AZURE_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ VK provider config key azure_key_config required fields match: [$CONFIG_VKPC_AZURE_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check VK provider config key vertex_key_config required fields
|
||||
CONFIG_VKPC_VERTEX_REQUIRED=$(jq -r '."$defs".virtual_key_provider_config.properties.keys.items.properties.vertex_key_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_VKPC_VERTEX_REQUIRED=$(jq -r '."$defs".virtualKeyProviderConfig.properties.keys.items.properties.vertex_key_config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_VKPC_VERTEX_REQUIRED" != "$HELM_VKPC_VERTEX_REQUIRED" ]; then
|
||||
echo "❌ VK provider config key vertex_key_config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_VKPC_VERTEX_REQUIRED]"
|
||||
echo " Helm: [$HELM_VKPC_VERTEX_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ VK provider config key vertex_key_config required fields match: [$CONFIG_VKPC_VERTEX_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check VK provider config key vllm_key_config required fields
|
||||
CONFIG_VKPC_VLLM_REQUIRED=$(jq -r '."$defs".virtual_key_provider_config.properties.keys.items.properties.vllm_key_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_VKPC_VLLM_REQUIRED=$(jq -r '."$defs".virtualKeyProviderConfig.properties.keys.items.properties.vllm_key_config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_VKPC_VLLM_REQUIRED" != "$HELM_VKPC_VLLM_REQUIRED" ]; then
|
||||
echo "❌ VK provider config key vllm_key_config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_VKPC_VLLM_REQUIRED]"
|
||||
echo " Helm: [$HELM_VKPC_VLLM_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ VK provider config key vllm_key_config required fields match: [$CONFIG_VKPC_VLLM_REQUIRED]"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔍 Checking required fields in virtual key MCP config..."
|
||||
|
||||
# Check virtual_key_mcp_config required fields
|
||||
CONFIG_VK_MCP_REQUIRED=$(jq -r '."$defs".virtual_key_mcp_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_VK_MCP_REQUIRED=$(jq -r '.properties.bifrost.properties.governance.properties.virtualKeys.items.properties.mcp_configs.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_VK_MCP_REQUIRED" != "$HELM_VK_MCP_REQUIRED" ]; then
|
||||
echo "❌ Virtual key MCP config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_VK_MCP_REQUIRED]"
|
||||
echo " Helm: [$HELM_VK_MCP_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Virtual key MCP config required fields match: [$CONFIG_VK_MCP_REQUIRED]"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔍 Checking required fields in MCP sub-configs..."
|
||||
|
||||
# Check MCP stdio_config required fields
|
||||
CONFIG_MCP_STDIO_REQUIRED=$(jq -r '."$defs".mcp_client_config.properties.stdio_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_MCP_STDIO_REQUIRED=$(jq -r '."$defs".mcpClientConfig.properties.stdioConfig.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_MCP_STDIO_REQUIRED" != "$HELM_MCP_STDIO_REQUIRED" ]; then
|
||||
echo "❌ MCP stdio config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_MCP_STDIO_REQUIRED]"
|
||||
echo " Helm: [$HELM_MCP_STDIO_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ MCP stdio config required fields match: [$CONFIG_MCP_STDIO_REQUIRED]"
|
||||
fi
|
||||
|
||||
# MCP websocket_config and http_config were removed from config.schema.json
|
||||
# because the corresponding Go fields don't exist (MCP rendering uses
|
||||
# connection_type + connection_string directly, not sub-object configs).
|
||||
# Helm still declares them for user convenience — not a schema sync concern.
|
||||
|
||||
echo ""
|
||||
echo "🔍 Checking required fields in SAML/SCIM config..."
|
||||
|
||||
# Check okta_config required fields
|
||||
CONFIG_OKTA_REQUIRED=$(jq -r '."$defs".okta_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_OKTA_REQUIRED=$(jq -r '.properties.bifrost.properties.scim.allOf[0].then.properties.config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_OKTA_REQUIRED" != "$HELM_OKTA_REQUIRED" ]; then
|
||||
echo "❌ Okta config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_OKTA_REQUIRED]"
|
||||
echo " Helm: [$HELM_OKTA_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Okta config required fields match: [$CONFIG_OKTA_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check entra_config required fields
|
||||
CONFIG_ENTRA_REQUIRED=$(jq -r '."$defs".entra_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_ENTRA_REQUIRED=$(jq -r '.properties.bifrost.properties.scim.allOf[1].then.properties.config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_ENTRA_REQUIRED" != "$HELM_ENTRA_REQUIRED" ]; then
|
||||
echo "❌ Entra config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_ENTRA_REQUIRED]"
|
||||
echo " Helm: [$HELM_ENTRA_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Entra config required fields match: [$CONFIG_ENTRA_REQUIRED]"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔍 Checking required fields in plugin configs..."
|
||||
|
||||
# Check semantic cache plugin required fields (dimension)
|
||||
# Config uses an allOf pattern on plugins array items; Helm uses conditional on semanticCache.enabled
|
||||
CONFIG_SEMCACHE_REQUIRED=$(jq -r '.properties.plugins.items.allOf[] | select(.if.properties.name.const == "semantic_cache") | .then.properties.config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_SEMCACHE_REQUIRED=$(jq -r '.properties.bifrost.properties.plugins.properties.semanticCache.then.properties.config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_SEMCACHE_REQUIRED" != "$HELM_SEMCACHE_REQUIRED" ]; then
|
||||
echo "❌ Semantic cache plugin config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_SEMCACHE_REQUIRED]"
|
||||
echo " Helm: [$HELM_SEMCACHE_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Semantic cache plugin config required fields match: [$CONFIG_SEMCACHE_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check OTEL plugin required fields (collector_url, trace_type, protocol)
|
||||
CONFIG_OTEL_REQUIRED=$(jq -r '.properties.plugins.items.allOf[] | select(.if.properties.name.const == "otel") | .then.properties.config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_OTEL_REQUIRED=$(jq -r '.properties.bifrost.properties.plugins.properties.otel.then.properties.config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_OTEL_REQUIRED" != "$HELM_OTEL_REQUIRED" ]; then
|
||||
echo "❌ OTEL plugin config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_OTEL_REQUIRED]"
|
||||
echo " Helm: [$HELM_OTEL_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ OTEL plugin config required fields match: [$CONFIG_OTEL_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check telemetry push_gateway required fields
|
||||
CONFIG_PUSHGW_REQUIRED=$(jq -r '.properties.plugins.items.allOf[] | select(.if.properties.name.const == "telemetry") | .then.properties.config.properties.push_gateway.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_PUSHGW_REQUIRED=$(jq -r '.properties.bifrost.properties.plugins.properties.telemetry.properties.config.properties.push_gateway.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_PUSHGW_REQUIRED" != "$HELM_PUSHGW_REQUIRED" ]; then
|
||||
echo "❌ Telemetry push_gateway required fields mismatch:"
|
||||
echo " Config: [$CONFIG_PUSHGW_REQUIRED]"
|
||||
echo " Helm: [$HELM_PUSHGW_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Telemetry push_gateway required fields match: [$CONFIG_PUSHGW_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check telemetry push_gateway basic_auth required fields
|
||||
CONFIG_PUSHGW_AUTH_REQUIRED=$(jq -r '.properties.plugins.items.allOf[] | select(.if.properties.name.const == "telemetry") | .then.properties.config.properties.push_gateway.properties.basic_auth.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_PUSHGW_AUTH_REQUIRED=$(jq -r '.properties.bifrost.properties.plugins.properties.telemetry.properties.config.properties.push_gateway.properties.basic_auth.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_PUSHGW_AUTH_REQUIRED" != "$HELM_PUSHGW_AUTH_REQUIRED" ]; then
|
||||
echo "❌ Telemetry push_gateway basic_auth required fields mismatch:"
|
||||
echo " Config: [$CONFIG_PUSHGW_AUTH_REQUIRED]"
|
||||
echo " Helm: [$HELM_PUSHGW_AUTH_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Telemetry push_gateway basic_auth required fields match: [$CONFIG_PUSHGW_AUTH_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check plugin array items required fields (enabled, name)
|
||||
# Config defines plugins as an array; Helm splits into named plugins + a "custom" array
|
||||
CONFIG_PLUGIN_ITEMS_REQUIRED=$(jq -r '.properties.plugins.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_PLUGIN_ITEMS_REQUIRED=$(jq -r '.properties.bifrost.properties.plugins.properties.custom.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_PLUGIN_ITEMS_REQUIRED" != "$HELM_PLUGIN_ITEMS_REQUIRED" ]; then
|
||||
echo "❌ Plugin items required fields mismatch:"
|
||||
echo " Config (plugins.items): [$CONFIG_PLUGIN_ITEMS_REQUIRED]"
|
||||
echo " Helm (plugins.custom.items): [$HELM_PLUGIN_ITEMS_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Plugin items required fields match: [$CONFIG_PLUGIN_ITEMS_REQUIRED]"
|
||||
fi
|
||||
|
||||
# Check plugin item properties completeness (all config properties must exist in helm custom items)
|
||||
echo ""
|
||||
echo "🔍 Checking plugin item property completeness..."
|
||||
|
||||
CONFIG_PLUGIN_PROPS=$(jq -r '.properties.plugins.items.properties | keys | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_CUSTOM_PLUGIN_PROPS=$(jq -r '.properties.bifrost.properties.plugins.properties.custom.items.properties | keys | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
# Check each config property exists in helm custom items
|
||||
for prop in $(echo "$CONFIG_PLUGIN_PROPS" | tr ',' '\n'); do
|
||||
if ! echo "$HELM_CUSTOM_PLUGIN_PROPS" | tr ',' '\n' | grep -qx "$prop"; then
|
||||
echo "❌ Plugin property '$prop' exists in config.schema.json but missing from helm custom plugin items"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Plugin property '$prop' present in both schemas"
|
||||
fi
|
||||
done
|
||||
|
||||
# Verify placement enum values match
|
||||
CONFIG_PLACEMENT_ENUM=$(jq -r '.properties.plugins.items.properties.placement.enum // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_PLACEMENT_ENUM=$(jq -r '.properties.bifrost.properties.plugins.properties.custom.items.properties.placement.enum // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_PLACEMENT_ENUM" != "$HELM_PLACEMENT_ENUM" ]; then
|
||||
echo "❌ Plugin placement enum mismatch:"
|
||||
echo " Config: [$CONFIG_PLACEMENT_ENUM]"
|
||||
echo " Helm: [$HELM_PLACEMENT_ENUM]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Plugin placement enum values match: [$CONFIG_PLACEMENT_ENUM]"
|
||||
fi
|
||||
|
||||
# Check maxim plugin config required fields (api_key)
|
||||
# Note: Helm allows either config.api_key OR secretRef.name via anyOf
|
||||
CONFIG_MAXIM_REQUIRED=$(jq -r '.properties.plugins.items.allOf[] | select(.if.properties.name.const == "maxim") | .then.properties.config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||
HELM_MAXIM_REQUIRED=$(jq -r '.properties.bifrost.properties.plugins.properties.maxim.then.anyOf[0].properties.config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CONFIG_MAXIM_REQUIRED" != "$HELM_MAXIM_REQUIRED" ]; then
|
||||
echo "❌ Maxim plugin config required fields mismatch:"
|
||||
echo " Config: [$CONFIG_MAXIM_REQUIRED]"
|
||||
echo " Helm (anyOf[0]): [$HELM_MAXIM_REQUIRED]"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "✅ Maxim plugin config required fields match: [$CONFIG_MAXIM_REQUIRED]"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔍 Checking property existence for Gap 1-8 fields..."
|
||||
|
||||
# Helper function to check a property exists in a schema
|
||||
check_property_exists() {
|
||||
local label=$1
|
||||
local jq_path=$2
|
||||
local schema_file=$3
|
||||
if ! jq -e "$jq_path" "$schema_file" > /dev/null 2>&1; then
|
||||
echo " ❌ Missing: $label"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo " ✅ Present: $label"
|
||||
fi
|
||||
}
|
||||
|
||||
# Gap 1+2: Client properties in Helm schema
|
||||
echo ""
|
||||
echo " Checking client properties (Gap 1+2)..."
|
||||
for prop in asyncJobResultTTL requiredHeaders loggingHeaders allowedHeaders mcpAgentDepth mcpToolExecutionTimeout mcpCodeModeBindingLevel mcpToolSyncInterval hideDeletedVirtualKeysInFilters; do
|
||||
check_property_exists "client.$prop" ".properties.bifrost.properties.client.properties.${prop}" "$HELM_SCHEMA"
|
||||
done
|
||||
|
||||
# Gap 3: OTel plugin config properties
|
||||
echo ""
|
||||
echo " Checking OTel plugin properties (Gap 3)..."
|
||||
for prop in headers tls_ca_cert insecure; do
|
||||
check_property_exists "otel.config.$prop" ".properties.bifrost.properties.plugins.properties.otel.properties.config.properties.${prop}" "$HELM_SCHEMA"
|
||||
done
|
||||
|
||||
# Gap 4: Governance plugin config properties
|
||||
echo ""
|
||||
echo " Checking governance plugin properties (Gap 4)..."
|
||||
for prop in required_headers is_enterprise; do
|
||||
check_property_exists "governance.plugin.config.$prop" ".properties.bifrost.properties.plugins.properties.governance.properties.config.properties.${prop}" "$HELM_SCHEMA"
|
||||
done
|
||||
|
||||
# Gap 5: Governance top-level properties
|
||||
echo ""
|
||||
echo " Checking governance top-level properties (Gap 5)..."
|
||||
for prop in modelConfigs providers; do
|
||||
check_property_exists "governance.$prop" ".properties.bifrost.properties.governance.properties.${prop}" "$HELM_SCHEMA"
|
||||
done
|
||||
|
||||
# Gap 6: MCP properties
|
||||
echo ""
|
||||
echo " Checking MCP properties (Gap 6)..."
|
||||
check_property_exists "mcp.toolSyncInterval" ".properties.bifrost.properties.mcp.properties.toolSyncInterval" "$HELM_SCHEMA"
|
||||
check_property_exists "mcp.toolManagerConfig.codeModeBindingLevel" '.properties.bifrost.properties.mcp.properties.toolManagerConfig.properties.codeModeBindingLevel' "$HELM_SCHEMA"
|
||||
for prop in clientId isCodeModeClient toolSyncInterval isPingAvailable; do
|
||||
check_property_exists "mcpClientConfig.$prop" '.["$defs"].mcpClientConfig.properties.'"${prop}" "$HELM_SCHEMA"
|
||||
done
|
||||
|
||||
# Gap 7: Cluster properties
|
||||
echo ""
|
||||
echo " Checking cluster properties (Gap 7)..."
|
||||
check_property_exists "cluster.region" ".properties.bifrost.properties.cluster.properties.region" "$HELM_SCHEMA"
|
||||
|
||||
# Gap 8: Miscellaneous properties
|
||||
echo ""
|
||||
echo " Checking miscellaneous properties (Gap 8)..."
|
||||
check_property_exists "telemetry.custom_labels" ".properties.bifrost.properties.plugins.properties.telemetry.properties.config.properties.custom_labels" "$HELM_SCHEMA"
|
||||
check_property_exists "semanticCache.default_cache_key" ".properties.bifrost.properties.plugins.properties.semanticCache.properties.config.properties.default_cache_key" "$HELM_SCHEMA"
|
||||
|
||||
# Also verify these exist in config.schema.json
|
||||
echo ""
|
||||
echo " Checking config.schema.json has is_ping_available + tool_pricing..."
|
||||
check_property_exists "mcp_client_config.is_ping_available" '."$defs".mcp_client_config.properties.is_ping_available' "$CONFIG_SCHEMA"
|
||||
check_property_exists "mcp_client_config.tool_pricing" '."$defs".mcp_client_config.properties.tool_pricing' "$CONFIG_SCHEMA"
|
||||
|
||||
echo ""
|
||||
if [ $ERRORS -gt 0 ]; then
|
||||
echo "❌ Schema validation failed with $ERRORS error(s)"
|
||||
echo ""
|
||||
echo "To fix these errors, update helm-charts/bifrost/values.schema.json to match"
|
||||
echo "the required fields in transports/config.schema.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All schema validations passed!"
|
||||
exit 0
|
||||
437
.github/workflows/scripts/validate-helm-templates.sh
vendored
Executable file
437
.github/workflows/scripts/validate-helm-templates.sh
vendored
Executable file
@@ -0,0 +1,437 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Helm template validation script for Bifrost
|
||||
# Validates all storage and vector store combinations render correctly
|
||||
|
||||
echo "🔍 Validating Helm Chart Templates..."
|
||||
echo "======================================"
|
||||
|
||||
# Color codes for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Track test results
|
||||
TESTS_PASSED=0
|
||||
TESTS_FAILED=0
|
||||
|
||||
# Function to report test result
|
||||
report_result() {
|
||||
local test_name=$1
|
||||
local result=$2
|
||||
|
||||
if [ "$result" -eq 0 ]; then
|
||||
echo -e "${GREEN}✅ $test_name${NC}"
|
||||
TESTS_PASSED=$((TESTS_PASSED + 1))
|
||||
else
|
||||
echo -e "${RED}❌ $test_name${NC}"
|
||||
TESTS_FAILED=$((TESTS_FAILED + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to test a helm template combination
|
||||
test_template() {
|
||||
local test_name=$1
|
||||
shift
|
||||
local helm_args=("$@")
|
||||
|
||||
if helm template bifrost ./helm-charts/bifrost \
|
||||
--set image.tag=v1.0.0 \
|
||||
"${helm_args[@]}" \
|
||||
> /tmp/helm-template-output.yaml 2>&1; then
|
||||
report_result "$test_name" 0
|
||||
return 0
|
||||
else
|
||||
report_result "$test_name" 1
|
||||
echo -e "${YELLOW} Error output:${NC}"
|
||||
head -10 /tmp/helm-template-output.yaml | sed 's/^/ /'
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 1. Storage Combinations (9 tests)
|
||||
echo ""
|
||||
echo -e "${CYAN}📦 1/6 - Testing Storage Combinations (9 tests)...${NC}"
|
||||
echo "---------------------------------------------------"
|
||||
|
||||
# config=no, logs=no
|
||||
test_template "config=no, logs=no" \
|
||||
--set storage.configStore.enabled=false \
|
||||
--set storage.logsStore.enabled=false \
|
||||
--set postgresql.enabled=false
|
||||
|
||||
# config=no, logs=sqlite
|
||||
test_template "config=no, logs=sqlite" \
|
||||
--set storage.configStore.enabled=false \
|
||||
--set storage.logsStore.enabled=true \
|
||||
--set storage.mode=sqlite \
|
||||
--set postgresql.enabled=false
|
||||
|
||||
# config=no, logs=postgres
|
||||
test_template "config=no, logs=postgres" \
|
||||
--set storage.configStore.enabled=false \
|
||||
--set storage.logsStore.enabled=true \
|
||||
--set storage.mode=postgres \
|
||||
--set postgresql.enabled=true \
|
||||
--set postgresql.auth.password=testpass
|
||||
|
||||
# config=sqlite, logs=no
|
||||
test_template "config=sqlite, logs=no" \
|
||||
--set storage.configStore.enabled=true \
|
||||
--set storage.logsStore.enabled=false \
|
||||
--set storage.mode=sqlite \
|
||||
--set postgresql.enabled=false
|
||||
|
||||
# config=sqlite, logs=sqlite
|
||||
test_template "config=sqlite, logs=sqlite" \
|
||||
--set storage.configStore.enabled=true \
|
||||
--set storage.logsStore.enabled=true \
|
||||
--set storage.mode=sqlite \
|
||||
--set postgresql.enabled=false
|
||||
|
||||
# config=sqlite, logs=postgres
|
||||
test_template "config=sqlite, logs=postgres" \
|
||||
--set storage.configStore.enabled=true \
|
||||
--set storage.logsStore.enabled=true \
|
||||
--set storage.mode=postgres \
|
||||
--set postgresql.enabled=true \
|
||||
--set postgresql.auth.password=testpass
|
||||
|
||||
# config=postgres, logs=no
|
||||
test_template "config=postgres, logs=no" \
|
||||
--set storage.configStore.enabled=true \
|
||||
--set storage.logsStore.enabled=false \
|
||||
--set storage.mode=postgres \
|
||||
--set postgresql.enabled=true \
|
||||
--set postgresql.auth.password=testpass
|
||||
|
||||
# config=postgres, logs=sqlite
|
||||
test_template "config=postgres, logs=sqlite" \
|
||||
--set storage.configStore.enabled=true \
|
||||
--set storage.logsStore.enabled=true \
|
||||
--set storage.mode=postgres \
|
||||
--set postgresql.enabled=true \
|
||||
--set postgresql.auth.password=testpass
|
||||
|
||||
# config=postgres, logs=postgres
|
||||
test_template "config=postgres, logs=postgres" \
|
||||
--set storage.configStore.enabled=true \
|
||||
--set storage.logsStore.enabled=true \
|
||||
--set storage.mode=postgres \
|
||||
--set postgresql.enabled=true \
|
||||
--set postgresql.auth.password=testpass
|
||||
|
||||
# 2. Vector Store Combinations (6 tests)
|
||||
echo ""
|
||||
echo -e "${CYAN}🗄️ 2/6 - Testing Vector Store Combinations (6 tests)...${NC}"
|
||||
echo "--------------------------------------------------------"
|
||||
|
||||
# Weaviate
|
||||
test_template "vectorStore=weaviate" \
|
||||
--set vectorStore.enabled=true \
|
||||
--set vectorStore.type=weaviate \
|
||||
--set vectorStore.weaviate.enabled=true
|
||||
|
||||
# Redis
|
||||
test_template "vectorStore=redis" \
|
||||
--set vectorStore.enabled=true \
|
||||
--set vectorStore.type=redis \
|
||||
--set vectorStore.redis.enabled=true
|
||||
|
||||
# Qdrant
|
||||
test_template "vectorStore=qdrant" \
|
||||
--set vectorStore.enabled=true \
|
||||
--set vectorStore.type=qdrant \
|
||||
--set vectorStore.qdrant.enabled=true
|
||||
|
||||
# postgres + weaviate
|
||||
test_template "postgres + weaviate" \
|
||||
--set storage.mode=postgres \
|
||||
--set postgresql.enabled=true \
|
||||
--set postgresql.auth.password=testpass \
|
||||
--set vectorStore.enabled=true \
|
||||
--set vectorStore.type=weaviate \
|
||||
--set vectorStore.weaviate.enabled=true
|
||||
|
||||
# postgres + qdrant
|
||||
test_template "postgres + qdrant" \
|
||||
--set storage.mode=postgres \
|
||||
--set postgresql.enabled=true \
|
||||
--set postgresql.auth.password=testpass \
|
||||
--set vectorStore.enabled=true \
|
||||
--set vectorStore.type=qdrant \
|
||||
--set vectorStore.qdrant.enabled=true
|
||||
|
||||
# sqlite + qdrant
|
||||
test_template "sqlite + qdrant" \
|
||||
--set storage.mode=sqlite \
|
||||
--set postgresql.enabled=false \
|
||||
--set vectorStore.enabled=true \
|
||||
--set vectorStore.type=qdrant \
|
||||
--set vectorStore.qdrant.enabled=true
|
||||
|
||||
# 3. Special Configurations (7 tests)
|
||||
echo ""
|
||||
echo -e "${CYAN}⚙️ 3/6 - Testing Special Configurations (7 tests)...${NC}"
|
||||
echo "-----------------------------------------------------"
|
||||
|
||||
# semantic cache: direct mode (dimension: 1, no provider/keys)
|
||||
test_template "semanticCache: direct mode (dimension: 1)" \
|
||||
--set bifrost.plugins.semanticCache.enabled=true \
|
||||
--set bifrost.plugins.semanticCache.config.dimension=1 \
|
||||
--set bifrost.plugins.semanticCache.config.ttl=30m \
|
||||
--set vectorStore.enabled=true \
|
||||
--set vectorStore.type=redis \
|
||||
--set vectorStore.redis.enabled=true
|
||||
|
||||
# semantic cache: semantic mode (dimension > 1, requires provider/keys)
|
||||
test_template "semanticCache: semantic mode (dimension: 1536)" \
|
||||
--set bifrost.plugins.semanticCache.enabled=true \
|
||||
--set bifrost.plugins.semanticCache.config.dimension=1536 \
|
||||
--set bifrost.plugins.semanticCache.config.provider=openai \
|
||||
--set 'bifrost.plugins.semanticCache.config.keys[0]=sk-test' \
|
||||
--set vectorStore.enabled=true \
|
||||
--set vectorStore.type=redis \
|
||||
--set vectorStore.redis.enabled=true
|
||||
|
||||
# semantic cache: direct mode with redis + postgres
|
||||
test_template "semanticCache: direct mode + postgres" \
|
||||
--set bifrost.plugins.semanticCache.enabled=true \
|
||||
--set bifrost.plugins.semanticCache.config.dimension=1 \
|
||||
--set storage.mode=postgres \
|
||||
--set postgresql.enabled=true \
|
||||
--set postgresql.auth.password=testpass \
|
||||
--set vectorStore.enabled=true \
|
||||
--set vectorStore.type=redis \
|
||||
--set vectorStore.redis.enabled=true
|
||||
|
||||
# sqlite + persistence + autoscaling (StatefulSet HPA)
|
||||
test_template "sqlite + persistence + autoscaling (StatefulSet)" \
|
||||
--set storage.mode=sqlite \
|
||||
--set storage.persistence.enabled=true \
|
||||
--set postgresql.enabled=false \
|
||||
--set autoscaling.enabled=true \
|
||||
--set autoscaling.minReplicas=2 \
|
||||
--set autoscaling.maxReplicas=5
|
||||
|
||||
# postgres + autoscaling (Deployment HPA)
|
||||
test_template "postgres + autoscaling (Deployment)" \
|
||||
--set storage.mode=postgres \
|
||||
--set postgresql.enabled=true \
|
||||
--set postgresql.auth.password=testpass \
|
||||
--set autoscaling.enabled=true \
|
||||
--set autoscaling.minReplicas=2 \
|
||||
--set autoscaling.maxReplicas=5
|
||||
|
||||
# ingress enabled
|
||||
test_template "ingress enabled" \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.className=nginx \
|
||||
--set 'ingress.hosts[0].host=bifrost.example.com' \
|
||||
--set 'ingress.hosts[0].paths[0].path=/' \
|
||||
--set 'ingress.hosts[0].paths[0].pathType=Prefix'
|
||||
|
||||
# full production-like config
|
||||
test_template "production-like config" \
|
||||
--set storage.mode=postgres \
|
||||
--set postgresql.enabled=true \
|
||||
--set postgresql.auth.password=testpass \
|
||||
--set vectorStore.enabled=true \
|
||||
--set vectorStore.type=qdrant \
|
||||
--set vectorStore.qdrant.enabled=true \
|
||||
--set autoscaling.enabled=true \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.className=nginx \
|
||||
--set 'ingress.hosts[0].host=bifrost.example.com' \
|
||||
--set 'ingress.hosts[0].paths[0].path=/' \
|
||||
--set 'ingress.hosts[0].paths[0].pathType=Prefix'
|
||||
|
||||
# 4. New Property Rendering (Gap 1-8 tests)
|
||||
echo ""
|
||||
echo -e "${CYAN}🆕 4/6 - Testing New Property Rendering (Gap 1-8)...${NC}"
|
||||
echo "-----------------------------------------------------"
|
||||
|
||||
# Gap 1+2: Client new properties
|
||||
test_template "client: new properties (Gap 1+2)" \
|
||||
--set bifrost.client.asyncJobResultTTL=300 \
|
||||
--set 'bifrost.client.requiredHeaders[0]=X-Request-ID' \
|
||||
--set 'bifrost.client.loggingHeaders[0]=X-Trace-ID' \
|
||||
--set 'bifrost.client.allowedHeaders[0]=Authorization' \
|
||||
--set bifrost.client.mcpAgentDepth=5 \
|
||||
--set bifrost.client.mcpToolExecutionTimeout=30 \
|
||||
--set bifrost.client.mcpCodeModeBindingLevel=server \
|
||||
--set bifrost.client.mcpToolSyncInterval=60 \
|
||||
--set bifrost.client.hideDeletedVirtualKeysInFilters=true
|
||||
|
||||
# Gap 3: OTel plugin with new fields
|
||||
test_template "otel: headers + tls_ca_cert + insecure (Gap 3)" \
|
||||
--set bifrost.plugins.otel.enabled=true \
|
||||
--set bifrost.plugins.otel.config.collector_url=otel:4317 \
|
||||
--set bifrost.plugins.otel.config.trace_type=genai_extension \
|
||||
--set bifrost.plugins.otel.config.protocol=grpc \
|
||||
--set 'bifrost.plugins.otel.config.headers.Authorization=Bearer token' \
|
||||
--set bifrost.plugins.otel.config.tls_ca_cert=/certs/ca.pem \
|
||||
--set bifrost.plugins.otel.config.insecure=true
|
||||
|
||||
# Gap 4: Governance plugin with new fields
|
||||
test_template "governance: required_headers + is_enterprise (Gap 4)" \
|
||||
--set bifrost.plugins.governance.enabled=true \
|
||||
--set 'bifrost.plugins.governance.config.required_headers[0]=X-Team-ID' \
|
||||
--set bifrost.plugins.governance.config.is_enterprise=true
|
||||
|
||||
# Gap 5: Governance modelConfigs + providers
|
||||
test_template "governance: modelConfigs + providers (Gap 5)" \
|
||||
--set 'bifrost.governance.modelConfigs[0].id=mc-1' \
|
||||
--set 'bifrost.governance.modelConfigs[0].model_name=gpt-4o' \
|
||||
--set 'bifrost.governance.providers[0].name=openai'
|
||||
|
||||
# Gap 6: MCP new fields
|
||||
test_template "mcp: toolSyncInterval + codeModeBindingLevel (Gap 6)" \
|
||||
--set bifrost.mcp.enabled=true \
|
||||
--set bifrost.mcp.toolSyncInterval=10m \
|
||||
--set bifrost.mcp.toolManagerConfig.codeModeBindingLevel=server \
|
||||
--set 'bifrost.mcp.clientConfigs[0].name=test' \
|
||||
--set 'bifrost.mcp.clientConfigs[0].connectionType=http' \
|
||||
--set 'bifrost.mcp.clientConfigs[0].httpConfig.url=http://localhost:3000' \
|
||||
--set 'bifrost.mcp.clientConfigs[0].clientId=client-1' \
|
||||
--set 'bifrost.mcp.clientConfigs[0].isCodeModeClient=true' \
|
||||
--set 'bifrost.mcp.clientConfigs[0].toolSyncInterval=5m'
|
||||
|
||||
# Gap 7: Cluster with region
|
||||
test_template "cluster: region (Gap 7)" \
|
||||
--set bifrost.cluster.enabled=true \
|
||||
--set 'bifrost.cluster.peers[0]=peer-0:7946' \
|
||||
--set bifrost.cluster.gossip.port=7946 \
|
||||
--set bifrost.cluster.gossip.config.timeoutSeconds=10 \
|
||||
--set bifrost.cluster.gossip.config.successThreshold=3 \
|
||||
--set bifrost.cluster.gossip.config.failureThreshold=3 \
|
||||
--set bifrost.cluster.region=us-east-1
|
||||
|
||||
# Gap 8: Combined production-like with all new fields
|
||||
test_template "combined: all new Gap 1-8 fields" \
|
||||
--set bifrost.client.asyncJobResultTTL=300 \
|
||||
--set bifrost.client.mcpAgentDepth=5 \
|
||||
--set bifrost.client.hideDeletedVirtualKeysInFilters=true \
|
||||
--set bifrost.plugins.otel.enabled=true \
|
||||
--set bifrost.plugins.otel.config.collector_url=otel:4317 \
|
||||
--set bifrost.plugins.otel.config.trace_type=genai_extension \
|
||||
--set bifrost.plugins.otel.config.protocol=grpc \
|
||||
--set bifrost.plugins.otel.config.insecure=true \
|
||||
--set bifrost.plugins.governance.enabled=true \
|
||||
--set bifrost.plugins.governance.config.is_enterprise=true \
|
||||
--set bifrost.cluster.enabled=true \
|
||||
--set 'bifrost.cluster.peers[0]=peer-0:7946' \
|
||||
--set bifrost.cluster.gossip.port=7946 \
|
||||
--set bifrost.cluster.gossip.config.timeoutSeconds=10 \
|
||||
--set bifrost.cluster.gossip.config.successThreshold=3 \
|
||||
--set bifrost.cluster.gossip.config.failureThreshold=3 \
|
||||
--set bifrost.cluster.region=us-west-2
|
||||
|
||||
# 5. Plugin Name Validation
|
||||
echo ""
|
||||
echo -e "${CYAN}🔌 5/6 - Validating Plugin Names Match Go Registry...${NC}"
|
||||
echo "------------------------------------------------------"
|
||||
|
||||
# Verify semantic cache plugin renders with correct name ("semantic_cache", not "semantic_cache")
|
||||
# Go registry: plugins/semantic_cache/main.go defines PluginName = "semantic_cache"
|
||||
test_name="semanticCache plugin name matches Go registry (semantic_cache)"
|
||||
if helm template bifrost ./helm-charts/bifrost \
|
||||
--set image.tag=v1.0.0 \
|
||||
--set bifrost.plugins.semanticCache.enabled=true \
|
||||
--set bifrost.plugins.semanticCache.config.dimension=1536 \
|
||||
--set bifrost.plugins.semanticCache.config.provider=openai \
|
||||
--set 'bifrost.plugins.semanticCache.config.keys[0]=sk-test' \
|
||||
--set vectorStore.enabled=true \
|
||||
--set vectorStore.type=redis \
|
||||
--set vectorStore.redis.enabled=true \
|
||||
> /tmp/helm-template-output.yaml 2>&1; then
|
||||
if grep -Eq '"name"[[:space:]]*:[[:space:]]*"semantic_cache"' /tmp/helm-template-output.yaml; then
|
||||
report_result "$test_name" 0
|
||||
else
|
||||
report_result "$test_name" 1
|
||||
fi
|
||||
else
|
||||
report_result "$test_name" 1
|
||||
echo -e "${YELLOW} Error output:${NC}"
|
||||
head -10 /tmp/helm-template-output.yaml | sed 's/^/ /'
|
||||
fi
|
||||
|
||||
# 6. Custom Plugin Placement and Order Rendering
|
||||
echo ""
|
||||
echo -e "${CYAN}🔧 6/6 - Validating Custom Plugin placement and order Rendering...${NC}"
|
||||
echo "-------------------------------------------------------------------"
|
||||
|
||||
# Test custom plugin renders successfully with placement and order
|
||||
test_template "custom plugin with placement and order" \
|
||||
--set 'bifrost.plugins.custom[0].name=my-plugin' \
|
||||
--set 'bifrost.plugins.custom[0].enabled=true' \
|
||||
--set 'bifrost.plugins.custom[0].path=/plugins/my-plugin.so' \
|
||||
--set 'bifrost.plugins.custom[0].placement=pre_builtin' \
|
||||
--set 'bifrost.plugins.custom[0].order=2'
|
||||
|
||||
# Verify placement appears in rendered output
|
||||
test_name="custom plugin rendered JSON contains placement field"
|
||||
if helm template bifrost ./helm-charts/bifrost \
|
||||
--set image.tag=v1.0.0 \
|
||||
--set 'bifrost.plugins.custom[0].name=my-plugin' \
|
||||
--set 'bifrost.plugins.custom[0].enabled=true' \
|
||||
--set 'bifrost.plugins.custom[0].path=/plugins/my-plugin.so' \
|
||||
--set 'bifrost.plugins.custom[0].placement=pre_builtin' \
|
||||
--set 'bifrost.plugins.custom[0].order=2' \
|
||||
> /tmp/helm-template-output.yaml 2>&1; then
|
||||
if grep -Eq '"placement"[[:space:]]*:[[:space:]]*"pre_builtin"' /tmp/helm-template-output.yaml; then
|
||||
report_result "$test_name" 0
|
||||
else
|
||||
report_result "$test_name" 1
|
||||
echo -e "${YELLOW} placement field not found in rendered output${NC}"
|
||||
fi
|
||||
else
|
||||
report_result "$test_name" 1
|
||||
echo -e "${YELLOW} Error output:${NC}"
|
||||
head -10 /tmp/helm-template-output.yaml | sed 's/^/ /'
|
||||
fi
|
||||
|
||||
# Verify order appears in rendered output
|
||||
test_name="custom plugin rendered JSON contains order field"
|
||||
if helm template bifrost ./helm-charts/bifrost \
|
||||
--set image.tag=v1.0.0 \
|
||||
--set 'bifrost.plugins.custom[0].name=my-plugin' \
|
||||
--set 'bifrost.plugins.custom[0].enabled=true' \
|
||||
--set 'bifrost.plugins.custom[0].path=/plugins/my-plugin.so' \
|
||||
--set 'bifrost.plugins.custom[0].placement=pre_builtin' \
|
||||
--set 'bifrost.plugins.custom[0].order=2' \
|
||||
> /tmp/helm-template-output.yaml 2>&1; then
|
||||
if grep -Eq '"order"[[:space:]]*:[[:space:]]*2' /tmp/helm-template-output.yaml; then
|
||||
report_result "$test_name" 0
|
||||
else
|
||||
report_result "$test_name" 1
|
||||
echo -e "${YELLOW} order field not found in rendered output${NC}"
|
||||
fi
|
||||
else
|
||||
report_result "$test_name" 1
|
||||
echo -e "${YELLOW} Error output:${NC}"
|
||||
head -10 /tmp/helm-template-output.yaml | sed 's/^/ /'
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -f /tmp/helm-template-output.yaml
|
||||
|
||||
# Final Summary
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "🏁 Helm Template Validation Complete!"
|
||||
echo "======================================"
|
||||
echo -e "${GREEN}Passed: $TESTS_PASSED${NC}"
|
||||
echo -e "${RED}Failed: $TESTS_FAILED${NC}"
|
||||
echo ""
|
||||
|
||||
if [ "$TESTS_FAILED" -gt 0 ]; then
|
||||
echo -e "${RED}❌ Some template validations failed. Please review the output above.${NC}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}✅ All template validations passed successfully!${NC}"
|
||||
exit 0
|
||||
fi
|
||||
63
.github/workflows/scripts/validate-schema-sync.sh
vendored
Executable file
63
.github/workflows/scripts/validate-schema-sync.sh
vendored
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Validate that Go config types in transports/bifrost-http/lib/config.go
|
||||
# stay in sync (fields + enum values) with transports/config.schema.json.
|
||||
# Walks the type graph recursively via go/types rather than regex-parsing source.
|
||||
|
||||
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
else
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
fi
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
TOOL_DIR="$SCRIPT_DIR/schemasync"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
if ! command -v go >/dev/null 2>&1; then
|
||||
echo "❌ go toolchain required for schema-sync validation"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Ensure go.work exists at the repo root. schemasync's packages.Load needs
|
||||
# it to resolve bifrost's local modules against each other. On fresh CI
|
||||
# runners go.work is not checked in, so we provision it here inline.
|
||||
# Sibling scripts (test-bifrost-http.sh etc.) call setup-go-workspace.sh
|
||||
# via `source`, but that relies on the `return` builtin which has
|
||||
# platform-dependent edge cases under `set -e`; we instead do the same
|
||||
# work inline so this wrapper is self-contained.
|
||||
if [ ! -f "$REPO_ROOT/go.work" ]; then
|
||||
echo "🔧 Setting up Go workspace (go.work not found)..."
|
||||
(
|
||||
cd "$REPO_ROOT"
|
||||
go work init
|
||||
for mod in ./core ./framework \
|
||||
./plugins/compat ./plugins/governance ./plugins/jsonparser \
|
||||
./plugins/logging ./plugins/maxim ./plugins/mocker \
|
||||
./plugins/otel ./plugins/prompts ./plugins/semanticcache \
|
||||
./plugins/telemetry \
|
||||
./transports ./cli; do
|
||||
if [ -f "$REPO_ROOT/$mod/go.mod" ]; then
|
||||
go work use "$mod"
|
||||
fi
|
||||
done
|
||||
)
|
||||
echo "✅ Go workspace initialized at $REPO_ROOT/go.work"
|
||||
else
|
||||
echo "🔍 Go workspace already exists at $REPO_ROOT/go.work, skipping initialization"
|
||||
fi
|
||||
|
||||
echo "🔍 Validating Go ↔ config.schema.json sync (recursive, AST-based)"
|
||||
echo "=================================================================="
|
||||
|
||||
# The schemasync tool is its own module (separate go.mod). Build it with
|
||||
# GOWORK=off so the tool's deps (golang.org/x/tools) resolve against its
|
||||
# own go.mod, not the repo's go.work. At runtime the tool itself sets
|
||||
# GOWORK=<repo-root>/go.work when loading bifrost packages.
|
||||
(cd "$TOOL_DIR" && GOWORK=off go build -o /tmp/schemasync .)
|
||||
/tmp/schemasync \
|
||||
--schema "$REPO_ROOT/transports/config.schema.json" \
|
||||
--pkg-root "$REPO_ROOT" \
|
||||
--helm-values "$REPO_ROOT/helm-charts/bifrost/values.schema.json" \
|
||||
--helm-helpers "$REPO_ROOT/helm-charts/bifrost/templates/_helpers.tpl"
|
||||
73
.github/workflows/scripts/verify-bifrost-http-release.sh
vendored
Executable file
73
.github/workflows/scripts/verify-bifrost-http-release.sh
vendored
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to verify if bifrost-http was successfully released
|
||||
# This ensures Docker images are only built after a successful bifrost-http release
|
||||
# Exits with code 0 if release is verified or not needed, exits with code 78 to skip if release failed
|
||||
|
||||
set -e
|
||||
|
||||
VERSION=$1
|
||||
RELEASE_NEEDED=$2
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "❌ Error: Version not provided"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If release was not needed, skip verification
|
||||
if [ "$RELEASE_NEEDED" = "false" ]; then
|
||||
echo "ℹ️ Bifrost-http release was not needed, skipping verification"
|
||||
echo " Docker images will be built with existing version"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "🔍 Verifying bifrost-http release v${VERSION}..."
|
||||
|
||||
# Check if the git tag exists
|
||||
if ! git rev-parse "transports/bifrost-http/v${VERSION}" >/dev/null 2>&1; then
|
||||
echo "⚠️ Git tag transports/bifrost-http/v${VERSION} not found"
|
||||
echo " Bifrost-http release did not complete successfully"
|
||||
echo " Skipping Docker image build..."
|
||||
exit 78 # Exit code 78 will be used to skip the job
|
||||
fi
|
||||
|
||||
echo "✅ Git tag found: transports/bifrost-http/v${VERSION}"
|
||||
|
||||
# Check if the GitHub release exists
|
||||
if [ -n "$GH_TOKEN" ]; then
|
||||
echo "🔍 Checking GitHub release..."
|
||||
if gh release view "transports/bifrost-http/v${VERSION}" >/dev/null 2>&1; then
|
||||
echo "✅ GitHub release found for transports/bifrost-http/v${VERSION}"
|
||||
else
|
||||
echo "⚠️ GitHub release for transports/bifrost-http/v${VERSION} not found"
|
||||
echo " Bifrost-http release did not complete successfully"
|
||||
echo " Skipping Docker image build..."
|
||||
exit 78 # Exit code 78 will be used to skip the job
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Warning: GH_TOKEN not set, skipping GitHub release check"
|
||||
fi
|
||||
|
||||
# Check if dist binaries exist for the version
|
||||
echo "🔍 Checking if release binaries exist..."
|
||||
BINARY_FOUND=false
|
||||
|
||||
# Check for common binary paths
|
||||
for arch in "darwin/amd64" "darwin/arm64" "linux/amd64"; do
|
||||
BINARY_PATH="dist/${arch}/bifrost-http"
|
||||
if [ -f "$BINARY_PATH" ]; then
|
||||
echo "✅ Found binary: $BINARY_PATH"
|
||||
BINARY_FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$BINARY_FOUND" = false ]; then
|
||||
echo "⚠️ Warning: No release binaries found in dist/, but continuing..."
|
||||
echo " This might be expected if binaries are uploaded to external storage"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ Verification complete: bifrost-http v${VERSION} was successfully released"
|
||||
echo " Proceeding with Docker image build..."
|
||||
|
||||
159
.github/workflows/snyk.yml
vendored
Normal file
159
.github/workflows/snyk.yml
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
name: Snyk checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master, "**/*"]
|
||||
pull_request:
|
||||
branches: ["**/*"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
snyk-open-source:
|
||||
name: Snyk Open Source (deps)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.github.com:443
|
||||
api.snyk.io:443
|
||||
downloads.snyk.io:443
|
||||
files.pythonhosted.org:443
|
||||
fonts.googleapis.com:443
|
||||
fonts.gstatic.com:443
|
||||
github.com:443
|
||||
iojs.org:443
|
||||
nodejs.org:443
|
||||
packages.microsoft.com:443
|
||||
proxy.golang.org:443
|
||||
raw.githubusercontent.com:443
|
||||
registry.npmjs.org:443
|
||||
release-assets.githubusercontent.com:443
|
||||
releases.astral.sh:443
|
||||
static.snyk.io:443
|
||||
storage.googleapis.com:443
|
||||
sum.golang.org:443
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Node (for UI)
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "25"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
with:
|
||||
version: "0.11.0"
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Sync Python dependencies (integrations)
|
||||
working-directory: tests/integrations/python
|
||||
run: uv sync --frozen
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version: "1.26.2"
|
||||
|
||||
- name: Setup Go workspace
|
||||
run: make setup-workspace
|
||||
|
||||
- name: Build
|
||||
run: make build LOCAL=1
|
||||
|
||||
- name: Install Snyk CLI
|
||||
uses: maximhq/snyk-actions/setup@9adf32b1121593767fc3c057af55b55db032dc04 # v1.0.0
|
||||
with:
|
||||
snyk-version: v1.1303.2
|
||||
|
||||
- name: Snyk test (all projects)
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
run: snyk test --all-projects --detection-depth=4 --exclude=examples,tests --sarif-file-output=snyk.sarif || true
|
||||
|
||||
- name: Upload SARIF
|
||||
if: always() && hashFiles('snyk.sarif') != ''
|
||||
uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
with:
|
||||
sarif_file: snyk.sarif
|
||||
|
||||
snyk-code:
|
||||
name: Snyk Code (SAST)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.github.com:443
|
||||
api.snyk.io:443
|
||||
deeproxy.snyk.io:443
|
||||
downloads.snyk.io:443
|
||||
files.pythonhosted.org:443
|
||||
fonts.googleapis.com:443
|
||||
fonts.gstatic.com:443
|
||||
github.com:443
|
||||
iojs.org:443
|
||||
nodejs.org:443
|
||||
packages.microsoft.com:443
|
||||
proxy.golang.org:443
|
||||
raw.githubusercontent.com:443
|
||||
registry.npmjs.org:443
|
||||
release-assets.githubusercontent.com:443
|
||||
releases.astral.sh:443
|
||||
storage.googleapis.com:443
|
||||
sum.golang.org:443
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Node (for UI)
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "25"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
with:
|
||||
version: "0.11.0"
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Sync Python dependencies (integrations)
|
||||
working-directory: tests/integrations/python
|
||||
run: uv sync --frozen
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version: "1.26.2"
|
||||
|
||||
- name: Setup Go workspace
|
||||
run: make setup-workspace
|
||||
|
||||
- name: Build
|
||||
run: make build LOCAL=1
|
||||
|
||||
- name: Install Snyk CLI
|
||||
uses: maximhq/snyk-actions/setup@9adf32b1121593767fc3c057af55b55db032dc04 # v1.0.0
|
||||
with:
|
||||
snyk-version: v1.1303.2
|
||||
|
||||
- name: Snyk Code test
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
run: snyk code test --sarif-file-output=snyk-code.sarif || true
|
||||
|
||||
- name: Upload SARIF
|
||||
if: always() && hashFiles('snyk-code.sarif') != ''
|
||||
uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
with:
|
||||
sarif_file: snyk-code.sarif
|
||||
Reference in New Issue
Block a user