first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View 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
View 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"

View 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
}

View 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
View 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"

View 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

View 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}

View 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

View 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
View 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
View 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
}

View 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++"

View 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
}
}
}

View 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
View 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 "$@"

View 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"

View 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"

View 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 earlyexit 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

View 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"

View 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
View 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
View 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
View 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"

View 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
View 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"

View 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

View 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

File diff suppressed because it is too large Load Diff

172
.github/workflows/scripts/run-tests.sh vendored Executable file
View 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

View 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
)

View 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=

File diff suppressed because it is too large Load Diff

View 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
View 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 earlyexit 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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"

View 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..."