first commit
This commit is contained in:
361
.claude/skills/changelog-writer/SKILL.md
Normal file
361
.claude/skills/changelog-writer/SKILL.md
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
---
|
||||||
|
name: changelog-writer
|
||||||
|
description: Write changelogs for Bifrost releases. Reads git history, bumps module versions following the core→framework→plugins→transport hierarchy, writes transports/changelog.md (enterprise-style) and per-module changelog.md files, and updates version files. Invoked with /changelog-writer or /changelog-writer <transport-version>.
|
||||||
|
allowed-tools: Read, Grep, Glob, Bash, Edit, Write, Task, AskUserQuestion
|
||||||
|
---
|
||||||
|
|
||||||
|
# Changelog Writer
|
||||||
|
|
||||||
|
Generate changelogs for a new Bifrost release. Reads git history to identify changes per module, asks the user for version bump type, bumps all module versions respecting the dependency hierarchy, writes `transports/changelog.md` and per-module `changelog.md` files, and updates version files.
|
||||||
|
|
||||||
|
**IMPORTANT: This skill NEVER creates or modifies files under `docs/`.** No MDX files, no docs.json updates. Only `changelog.md` and `version` files within module directories.
|
||||||
|
|
||||||
|
## Module Hierarchy
|
||||||
|
|
||||||
|
Changes cascade down this dependency chain:
|
||||||
|
|
||||||
|
```
|
||||||
|
core → framework → plugins → transports
|
||||||
|
```
|
||||||
|
|
||||||
|
- **core** depends on nothing internal
|
||||||
|
- **framework** depends on core
|
||||||
|
- **plugins/*** each depend on core + framework
|
||||||
|
- **transports** depends on core + framework + all plugins
|
||||||
|
|
||||||
|
If core changes, every module below it must bump its version (at minimum a patch bump).
|
||||||
|
|
||||||
|
If framework changes (but not core), plugins and transports must bump.
|
||||||
|
|
||||||
|
If only a plugin changes, transports must bump.
|
||||||
|
|
||||||
|
If only transports changes, only transports bumps.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/changelog-writer # Interactive — prompts for everything
|
||||||
|
/changelog-writer <transport-ver> # Pre-set transport version (e.g., v1.5.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### Step 1: Gather Current State
|
||||||
|
|
||||||
|
Read the current version of every module:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "core: $(cat core/version)"
|
||||||
|
echo "framework: $(cat framework/version)"
|
||||||
|
echo "transports: $(cat transports/version)"
|
||||||
|
for d in plugins/*/; do echo "$(basename $d): $(cat ${d}version)"; done
|
||||||
|
```
|
||||||
|
|
||||||
|
Read the latest changelog file to understand the previous release state:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find the latest docs changelog to determine the last released version
|
||||||
|
ls -1t docs/changelogs/*.mdx | head -1
|
||||||
|
```
|
||||||
|
|
||||||
|
Then read that file to know the previous versions of all modules.
|
||||||
|
|
||||||
|
### Step 2: Identify Changes Since Last Release
|
||||||
|
|
||||||
|
Use git log to find commits since the last release tag or since the last changelog was written:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get the transport version from the latest changelog (it matches the release version)
|
||||||
|
LAST_VERSION=$(ls -1t docs/changelogs/*.mdx | head -1 | sed 's/.*\/v/v/' | sed 's/.mdx//')
|
||||||
|
echo "Last release: $LAST_VERSION"
|
||||||
|
|
||||||
|
# Check if a git tag exists
|
||||||
|
git tag -l "$LAST_VERSION" "v*"
|
||||||
|
|
||||||
|
# Get commits since last release
|
||||||
|
# If tag exists:
|
||||||
|
git log ${LAST_VERSION}..HEAD --oneline --no-merges
|
||||||
|
|
||||||
|
# If no tag, use date-based or commit-based approach:
|
||||||
|
# Find the commit that added the last changelog
|
||||||
|
git log --oneline --all -- "docs/changelogs/$(ls -1t docs/changelogs/*.mdx | head -1 | xargs basename)" | head -1
|
||||||
|
```
|
||||||
|
|
||||||
|
For each module, identify which files changed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Changes in core
|
||||||
|
git diff --name-only ${BASE}..HEAD -- core/
|
||||||
|
|
||||||
|
# Changes in framework
|
||||||
|
git diff --name-only ${BASE}..HEAD -- framework/
|
||||||
|
|
||||||
|
# Changes in each plugin
|
||||||
|
for d in plugins/*/; do
|
||||||
|
CHANGES=$(git diff --name-only ${BASE}..HEAD -- "$d" | wc -l)
|
||||||
|
if [ "$CHANGES" -gt 0 ]; then
|
||||||
|
echo "$(basename $d): $CHANGES files changed"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Changes in transports
|
||||||
|
git diff --name-only ${BASE}..HEAD -- transports/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Classify Changes and Determine Bump Types
|
||||||
|
|
||||||
|
Present the identified changes to the user and ask what type of version bump each changed module needs.
|
||||||
|
|
||||||
|
**Always ask the user with AskUserQuestion what bump type to use for each module.**
|
||||||
|
|
||||||
|
Ask for **every** module that will be bumped — both modules with code changes and modules with only cascade bumps. Use AskUserQuestion with up to 4 questions at a time (the tool's limit), batching in hierarchy order:
|
||||||
|
|
||||||
|
1. First batch: core, framework, and up to 2 plugins
|
||||||
|
2. Continue with remaining plugins and transports
|
||||||
|
|
||||||
|
For each module ask: "What type of version bump for **{module}**?"
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- **patch** — Bug fixes, small improvements (0.0.X)
|
||||||
|
- **minor** — New features, non-breaking changes (0.X.0)
|
||||||
|
- **major** — Breaking changes (X.0.0)
|
||||||
|
|
||||||
|
**Note:** Minor bumps reset the patch version to 0 (e.g., `1.4.24` → `1.5.0`). Patch bumps only increment the last number (e.g., `1.4.24` → `1.4.25`).
|
||||||
|
|
||||||
|
### Step 4: Calculate New Versions
|
||||||
|
|
||||||
|
Apply version bumps. Semver rules:
|
||||||
|
|
||||||
|
- **patch**: `1.4.4` → `1.4.5`
|
||||||
|
- **minor**: `1.4.4` → `1.5.0`
|
||||||
|
- **major**: `1.4.4` → `2.0.0`
|
||||||
|
|
||||||
|
Calculate new versions for ALL modules following the cascade rules:
|
||||||
|
|
||||||
|
```
|
||||||
|
new_core_version = bump(current_core, user_chosen_bump) if core changed, else current_core
|
||||||
|
new_framework_version = bump(current_framework, user_chosen_bump) if framework changed, else patch_bump(current_framework) if core changed, else current_framework
|
||||||
|
new_plugin_X_version = bump(current_plugin_X, user_chosen_bump) if plugin_X changed, else patch_bump(current_plugin_X) if core or framework changed, else current_plugin_X
|
||||||
|
new_transport_version = bump(current_transport, user_chosen_bump) if transport changed, else patch_bump(current_transport) if any upstream changed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Present the version plan to the user for confirmation before proceeding.**
|
||||||
|
|
||||||
|
Show a table like:
|
||||||
|
|
||||||
|
```
|
||||||
|
Module Current New Bump Type Reason
|
||||||
|
core 1.4.4 1.5.0 minor code changes
|
||||||
|
framework 1.2.23 1.3.0 minor cascade from core
|
||||||
|
governance 1.4.24 1.4.25 patch cascade from core+framework
|
||||||
|
...
|
||||||
|
transports 1.4.9 1.5.0 minor cascade from all
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait for user confirmation. If they want to adjust any version, update accordingly.
|
||||||
|
|
||||||
|
### Step 5: Collect and Write Changelog Entries
|
||||||
|
|
||||||
|
For each module, compose changelog entries from the git log.
|
||||||
|
|
||||||
|
**Read the actual git commits and changed code** to write meaningful entries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For each changed module, get detailed commit messages
|
||||||
|
git log ${BASE}..HEAD --oneline --no-merges -- core/
|
||||||
|
git log ${BASE}..HEAD --oneline --no-merges -- framework/
|
||||||
|
# etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Credit Outside Contributors
|
||||||
|
|
||||||
|
For each commit that references a PR number (e.g., `#1234`), check if the author is an outside contributor:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get the repo name
|
||||||
|
REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner')
|
||||||
|
|
||||||
|
# For each PR number found in commits:
|
||||||
|
gh api "repos/$REPO/pulls/<PR_NUMBER>" --jq '"\(.number) \(.user.login) \(.author_association)"'
|
||||||
|
```
|
||||||
|
|
||||||
|
**`author_association` values:**
|
||||||
|
- `MEMBER`, `OWNER`, `COLLABORATOR` → internal team, no credit needed
|
||||||
|
- `CONTRIBUTOR`, `FIRST_TIMER`, `FIRST_TIME_CONTRIBUTOR`, `NONE` → outside contributor, credit them
|
||||||
|
|
||||||
|
**How to credit:**
|
||||||
|
|
||||||
|
Use a markdown link to the contributor's GitHub profile: `[@username](https://github.com/username)`
|
||||||
|
|
||||||
|
- In **transports/changelog.md** (enterprise-style): append `(thanks [@username](https://github.com/username)!)` to the description
|
||||||
|
- Example: `- **Logprobs JSON Tag** — Fixed logprobs JSON tag in BifrostResponseChoice (thanks [@contributor](https://github.com/contributor)!)`
|
||||||
|
- In **per-module changelog.md** (flat-list): append `(thanks [@username](https://github.com/username)!)` to the entry
|
||||||
|
- Example: `- fix: fixed logprobs JSON tag in BifrostResponseChoice (thanks [@contributor](https://github.com/contributor)!)`
|
||||||
|
|
||||||
|
If multiple PRs from the same outside contributor are grouped into one entry, credit them once.
|
||||||
|
|
||||||
|
**Present the draft entries to the user for review before writing files.**
|
||||||
|
|
||||||
|
#### Per-Module changelog.md (core, framework, plugins)
|
||||||
|
|
||||||
|
Write simple flat-list entries to each module's `changelog.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
- fix: description of what was fixed
|
||||||
|
- feat: description of new feature
|
||||||
|
- hotfix: description of urgent fix
|
||||||
|
```
|
||||||
|
|
||||||
|
For modules with only cascading bumps (no code changes), add a `chore:` entry describing which upstream dependencies were bumped. **No changelog should ever be left empty.** Example:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
- chore: upgraded core to v1.5.0 and framework to v1.3.0
|
||||||
|
```
|
||||||
|
|
||||||
|
If only one upstream changed, mention just that one (e.g., `- chore: upgraded core to v1.5.0`). Always use the actual new versions of the upstream modules.
|
||||||
|
|
||||||
|
**Formatting rules for per-module changelogs:**
|
||||||
|
- Each entry starts with `- ` followed by the type prefix and colon
|
||||||
|
- Use `fix:`, `feat:`, `hotfix:`, or `chore:` prefixes
|
||||||
|
- Breaking changes get a `<Note>` or `<Warning>` block indented under the entry
|
||||||
|
- Keep entries concise — 1 line per change unless a breaking change note is needed
|
||||||
|
|
||||||
|
#### transports/changelog.md (Enterprise-Style Format)
|
||||||
|
|
||||||
|
The transports changelog uses a categorized format with bold names. Write it using this template:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- **Feature Name** — Description of the feature
|
||||||
|
- **Feature Name** — Description of the feature
|
||||||
|
|
||||||
|
## 🐞 Fixed
|
||||||
|
|
||||||
|
- **Bug Name** — Description of what was fixed
|
||||||
|
- **Bug Name** — Description of what was fixed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Formatting rules for transports/changelog.md:**
|
||||||
|
- Use `## ✨ Features` and `## 🐞 Fixed` section headers
|
||||||
|
- Each entry uses **bold name** followed by em dash (—) and description
|
||||||
|
- Keep descriptions concise — 1-2 lines max per bullet
|
||||||
|
- Group related commits into a single bullet point
|
||||||
|
- Include changes from ALL modules (transports is the top-level summary)
|
||||||
|
- Breaking changes get a `<Warning>` or `<Note>` block indented under the entry
|
||||||
|
- Omit sections that have no entries (e.g., if there are no features, skip the Features section)
|
||||||
|
- If the release has only cascading bumps and no meaningful features or fixes, add a `## 🔧 Maintenance` section with an entry like: `- **Dependency Upgrades** — Bumped core to v1.5.0 and framework to v1.3.0 across all modules`
|
||||||
|
|
||||||
|
### Step 6: Update Version Files
|
||||||
|
|
||||||
|
Update the `version` file in each module that was bumped:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "{new_version}" > core/version
|
||||||
|
echo "{new_version}" > framework/version
|
||||||
|
echo "{new_version}" > transports/version
|
||||||
|
echo "{new_version}" > plugins/{plugin}/version
|
||||||
|
```
|
||||||
|
|
||||||
|
**Do NOT update go.mod files** — that is handled separately by the developer as part of the release process.
|
||||||
|
|
||||||
|
### Step 7: Present Summary
|
||||||
|
|
||||||
|
After all files are written, present a summary:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Changelog Written: v{new_transport_version}
|
||||||
|
|
||||||
|
### Files Modified:
|
||||||
|
- transports/changelog.md
|
||||||
|
- core/changelog.md
|
||||||
|
- framework/changelog.md
|
||||||
|
- plugins/{changed_plugins}/changelog.md
|
||||||
|
- {list of version files updated}
|
||||||
|
|
||||||
|
### Version Bumps:
|
||||||
|
{table of old → new versions}
|
||||||
|
|
||||||
|
### Next Steps:
|
||||||
|
1. Review the changelogs
|
||||||
|
2. Update go.mod files with new dependency versions
|
||||||
|
3. Run `go mod tidy` in each module
|
||||||
|
4. Create the docs/changelogs MDX file and update docs.json manually
|
||||||
|
5. Tag the release: git tag v{new_transport_version}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### No Changes Detected
|
||||||
|
If git diff shows no changes since the last release:
|
||||||
|
```
|
||||||
|
No changes detected since the last release (v{last_version}).
|
||||||
|
Are you sure you want to create a new changelog?
|
||||||
|
```
|
||||||
|
Ask the user to confirm or provide a different base commit/tag.
|
||||||
|
|
||||||
|
### Version Conflict
|
||||||
|
If the calculated new version already has a changelog file in docs:
|
||||||
|
```
|
||||||
|
A changelog for v{version} already exists at docs/changelogs/v{version}.mdx.
|
||||||
|
Would you like to:
|
||||||
|
1. Continue anyway (version files and changelog.md will be overwritten)
|
||||||
|
2. Choose a different version number
|
||||||
|
```
|
||||||
|
|
||||||
|
### Missing Module Version File
|
||||||
|
If a version file is missing:
|
||||||
|
```bash
|
||||||
|
# Fallback: read version from go.mod
|
||||||
|
grep "^module" {module}/go.mod
|
||||||
|
```
|
||||||
|
Ask the user what version to use.
|
||||||
|
|
||||||
|
## Project Directory Reference
|
||||||
|
|
||||||
|
```
|
||||||
|
bifrost/
|
||||||
|
├── core/
|
||||||
|
│ ├── version # Plain text: "1.5.0"
|
||||||
|
│ ├── changelog.md # Simple flat-list format
|
||||||
|
│ └── go.mod
|
||||||
|
├── framework/
|
||||||
|
│ ├── version # Plain text: "1.3.0"
|
||||||
|
│ ├── changelog.md # Simple flat-list format
|
||||||
|
│ └── go.mod
|
||||||
|
├── plugins/
|
||||||
|
│ ├── governance/
|
||||||
|
│ │ ├── version
|
||||||
|
│ │ └── changelog.md # Simple flat-list format
|
||||||
|
│ ├── jsonparser/version
|
||||||
|
│ ├── litellmcompat/version
|
||||||
|
│ ├── logging/
|
||||||
|
│ │ ├── version
|
||||||
|
│ │ └── changelog.md # Simple flat-list format
|
||||||
|
│ ├── maxim/version
|
||||||
|
│ ├── mocker/version
|
||||||
|
│ ├── otel/version
|
||||||
|
│ ├── semanticcache/version
|
||||||
|
│ └── telemetry/version
|
||||||
|
├── transports/
|
||||||
|
│ ├── version # Plain text: "1.5.0"
|
||||||
|
│ ├── changelog.md # Enterprise-style format (✨ Features / 🐞 Fixed)
|
||||||
|
│ └── go.mod
|
||||||
|
└── docs/
|
||||||
|
├── changelogs/ # ⚠️ DO NOT TOUCH — MDX files managed separately
|
||||||
|
└── docs.json # ⚠️ DO NOT TOUCH — navigation managed separately
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugin List (Alphabetical Order)
|
||||||
|
|
||||||
|
This is the canonical order for plugins:
|
||||||
|
|
||||||
|
1. governance
|
||||||
|
2. jsonparser
|
||||||
|
3. litellmcompat
|
||||||
|
4. logging
|
||||||
|
5. maxim
|
||||||
|
6. mocker
|
||||||
|
7. otel
|
||||||
|
8. semanticcache
|
||||||
|
9. telemetry
|
||||||
859
.claude/skills/docs-writer/SKILL.md
Normal file
859
.claude/skills/docs-writer/SKILL.md
Normal file
@@ -0,0 +1,859 @@
|
|||||||
|
---
|
||||||
|
name: docs-writer
|
||||||
|
description: Write, update, and review Mintlify MDX documentation for Bifrost features. Explores the full codebase (UI, Go backend, config schema), validates config.json examples, places screenshot placeholders, and presents outlines for approval before writing. Invoked with /docs-writer <feature-name>, /docs-writer update <doc-path>, or /docs-writer review <doc-path>.
|
||||||
|
allowed-tools: Read, Grep, Glob, Bash, Edit, Write, WebSearch, WebFetch, mcp__context7__resolve-library-id, mcp__context7__query-docs, Task, AskUserQuestion, TodoWrite
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bifrost Documentation Writer
|
||||||
|
|
||||||
|
Write, update, and review Mintlify MDX documentation for Bifrost features. Performs thorough codebase research across both the React UI and Go backend, validates config.json examples against the schema, and follows established documentation conventions.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/docs-writer <feature-name> # Write new docs for a feature
|
||||||
|
/docs-writer update <doc-path> # Update an existing doc page
|
||||||
|
/docs-writer review <doc-path> # Review a doc for accuracy and completeness
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow Overview
|
||||||
|
|
||||||
|
1. **Understand the request** -- Determine which feature needs documentation
|
||||||
|
2. **Research the codebase** -- Explore UI pages, Go handlers, config schema, and existing docs
|
||||||
|
3. **Research external context** -- Use Context7 and WebSearch for additional information
|
||||||
|
4. **Ask clarifying questions** -- Confirm scope, audience, and edge cases with the user
|
||||||
|
5. **Present doc outline** -- Show a structured outline and wait for approval
|
||||||
|
6. **Write the documentation** -- Create the MDX file following Mintlify conventions
|
||||||
|
7. **Update navigation** -- Add the new page to docs.json
|
||||||
|
8. **Present for review** -- Show the complete doc and incorporate feedback
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Understand the Request
|
||||||
|
|
||||||
|
### For new docs (`/docs-writer <feature-name>`)
|
||||||
|
|
||||||
|
Parse the feature name and map it to codebase areas. Common feature-to-directory mappings:
|
||||||
|
|
||||||
|
| Feature Name | UI Directory | Handler File | Config Schema Section | Existing Docs |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| virtual-keys | `ui/app/workspace/virtual-keys/` | `handlers/governance.go` | `governance.virtual_keys` | `docs/features/governance/virtual-keys.mdx` |
|
||||||
|
| routing | `ui/app/workspace/routing-rules/` | `handlers/governance.go` | `governance.routing_rules` | `docs/features/governance/routing.mdx` |
|
||||||
|
| providers | `ui/app/workspace/providers/` | `handlers/providers.go` | `providers` | `docs/providers/` |
|
||||||
|
| mcp | `ui/app/workspace/mcp-registry/` | `handlers/mcp.go` | `mcp` | `docs/mcp/` |
|
||||||
|
| plugins | `ui/app/workspace/plugins/` | `handlers/plugins.go` | `plugins` | `docs/features/plugins/` |
|
||||||
|
| logs / observability | `ui/app/workspace/logs/` | `handlers/logging.go` | `client.enable_logging` | `docs/features/observability/` |
|
||||||
|
| semantic-caching | `ui/app/workspace/config/caching/` | `handlers/cache.go` | `plugins.semantic_cache` + `vector_store` | `docs/features/semantic-caching.mdx` |
|
||||||
|
| guardrails | `ui/app/workspace/guardrails/` | Enterprise | `guardrails_config` | `docs/enterprise/guardrails.mdx` |
|
||||||
|
| clustering | `ui/app/workspace/cluster/` | Enterprise | `cluster_config` | `docs/enterprise/clustering.mdx` |
|
||||||
|
| load-balancing | `ui/app/workspace/adaptive-routing/` | Enterprise | `load_balancer_config` | `docs/enterprise/adaptive-load-balancing.mdx` |
|
||||||
|
| audit-logs | `ui/app/workspace/audit-logs/` | Enterprise | `audit_logs` | `docs/enterprise/audit-logs.mdx` |
|
||||||
|
| rbac | `ui/app/workspace/rbac/` | Enterprise | `auth_config` | `docs/enterprise/rbac.mdx` |
|
||||||
|
| config | `ui/app/workspace/config/` | `handlers/config.go` | `client` | `docs/quickstart/` |
|
||||||
|
| budget-and-limits | `ui/app/workspace/virtual-keys/` | `handlers/governance.go` | `governance.budgets`, `governance.rate_limits` | `docs/features/governance/budget-and-limits.mdx` |
|
||||||
|
| mcp-tools | `ui/app/workspace/mcp-tool-groups/` | `handlers/governance.go` | `governance.virtual_keys[].mcp_configs` | `docs/features/governance/mcp-tools.mdx` |
|
||||||
|
| fallbacks | N/A (request-level) | `handlers/inference.go` | N/A | `docs/features/fallbacks.mdx` |
|
||||||
|
| telemetry | `ui/app/workspace/observability/` | `handlers/plugins.go` | `plugins.prometheus`, `plugins.otel` | `docs/features/telemetry.mdx` |
|
||||||
|
|
||||||
|
If the feature name does not match any known mapping, search the codebase:
|
||||||
|
```bash
|
||||||
|
# Search UI for the feature
|
||||||
|
ls ui/app/workspace/ | grep -i "<feature>"
|
||||||
|
|
||||||
|
# Search handlers for related endpoints
|
||||||
|
grep -rn "<feature>" transports/bifrost-http/handlers/ --include='*.go' -l
|
||||||
|
|
||||||
|
# Search config schema
|
||||||
|
grep -i "<feature>" transports/config.schema.json | head -20
|
||||||
|
|
||||||
|
# Search existing docs
|
||||||
|
grep -rn "<feature>" docs/ --include='*.mdx' -l
|
||||||
|
```
|
||||||
|
|
||||||
|
### For updates (`/docs-writer update <doc-path>`)
|
||||||
|
|
||||||
|
Read the existing doc first:
|
||||||
|
```bash
|
||||||
|
cat docs/<doc-path>
|
||||||
|
```
|
||||||
|
|
||||||
|
Then identify what has changed by checking recent git history:
|
||||||
|
```bash
|
||||||
|
# Find recent code changes related to the feature
|
||||||
|
git log --oneline -20 -- 'ui/app/workspace/<feature>/' 'transports/bifrost-http/handlers/<handler>.go'
|
||||||
|
|
||||||
|
# See actual diffs
|
||||||
|
git diff HEAD~10 -- 'ui/app/workspace/<feature>/'
|
||||||
|
```
|
||||||
|
|
||||||
|
### For reviews (`/docs-writer review <doc-path>`)
|
||||||
|
|
||||||
|
Read the doc and cross-reference against the current codebase to identify:
|
||||||
|
- Outdated information (API endpoints changed, UI flow changed)
|
||||||
|
- Missing config.json fields (new schema properties not documented)
|
||||||
|
- Missing screenshots for new UI elements
|
||||||
|
- Broken internal links
|
||||||
|
- config.json examples that do not match the schema
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Research the Codebase
|
||||||
|
|
||||||
|
**ALWAYS perform thorough research before writing.** This is the most critical step.
|
||||||
|
|
||||||
|
### 2a. Explore the UI Code
|
||||||
|
|
||||||
|
The UI is a React + Vite + TanStack Router application. Feature pages live under `ui/app/workspace/<feature>/`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List the feature directory structure
|
||||||
|
find ui/app/workspace/<feature>/ -type f -name '*.tsx' -o -name '*.ts' | sort
|
||||||
|
|
||||||
|
# Read the main page
|
||||||
|
cat ui/app/workspace/<feature>/page.tsx
|
||||||
|
|
||||||
|
# Find all views/components
|
||||||
|
ls ui/app/workspace/<feature>/views/ 2>/dev/null
|
||||||
|
ls ui/app/workspace/<feature>/dialogs/ 2>/dev/null
|
||||||
|
ls ui/app/workspace/<feature>/fragments/ 2>/dev/null
|
||||||
|
|
||||||
|
# Look for form fields, buttons, and interactive elements
|
||||||
|
grep -rn 'label\|placeholder\|data-testid\|FormField\|Input\|Select\|Button' ui/app/workspace/<feature>/ --include='*.tsx' | head -40
|
||||||
|
|
||||||
|
# Find API calls from the UI
|
||||||
|
grep -rn 'fetch\|api/\|useSWR\|mutate' ui/app/workspace/<feature>/ --include='*.tsx' --include='*.ts' | head -20
|
||||||
|
|
||||||
|
# Check shared components used
|
||||||
|
grep -rn 'import.*from.*components' ui/app/workspace/<feature>/ --include='*.tsx' | head -20
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to extract from UI research:**
|
||||||
|
- Feature name as shown in the UI (page title, sidebar label)
|
||||||
|
- All form fields for create/edit operations (field names, types, validation)
|
||||||
|
- Table columns and displayed data
|
||||||
|
- Available actions (create, edit, delete, import, export)
|
||||||
|
- Navigation flow (how users reach this feature)
|
||||||
|
- Any special UI patterns (sheets, dialogs, tabs, accordions)
|
||||||
|
|
||||||
|
### 2b. Explore the Go Backend
|
||||||
|
|
||||||
|
API handlers live in `transports/bifrost-http/handlers/`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find the relevant handler file
|
||||||
|
grep -rn '<feature>\|<Feature>' transports/bifrost-http/handlers/ --include='*.go' -l
|
||||||
|
|
||||||
|
# Read route registrations
|
||||||
|
grep -n '/api/' transports/bifrost-http/handlers/<handler>.go
|
||||||
|
|
||||||
|
# Read request/response types
|
||||||
|
grep -n 'type.*Request\|type.*Response' transports/bifrost-http/handlers/<handler>.go
|
||||||
|
|
||||||
|
# Read the handler functions for create/update operations
|
||||||
|
grep -n 'func.*create\|func.*update\|func.*delete\|func.*get' transports/bifrost-http/handlers/<handler>.go
|
||||||
|
```
|
||||||
|
|
||||||
|
**Complete API route reference by handler:**
|
||||||
|
|
||||||
|
| Handler File | Route Prefix | Operations |
|
||||||
|
|---|---|---|
|
||||||
|
| `governance.go` | `/api/governance/virtual-keys` | CRUD virtual keys |
|
||||||
|
| `governance.go` | `/api/governance/teams` | CRUD teams |
|
||||||
|
| `governance.go` | `/api/governance/customers` | CRUD customers |
|
||||||
|
| `governance.go` | `/api/governance/routing-rules` | CRUD routing rules |
|
||||||
|
| `governance.go` | `/api/governance/model-configs` | CRUD model configs |
|
||||||
|
| `governance.go` | `/api/governance/providers` | GET/PUT/DELETE provider governance |
|
||||||
|
| `governance.go` | `/api/governance/budgets` | GET budgets |
|
||||||
|
| `governance.go` | `/api/governance/rate-limits` | GET rate limits |
|
||||||
|
| `providers.go` | `/api/providers` | CRUD providers |
|
||||||
|
| `providers.go` | `/api/keys` | GET keys |
|
||||||
|
| `providers.go` | `/api/models` | GET models |
|
||||||
|
| `mcp.go` | `/api/mcp/clients` | GET MCP clients |
|
||||||
|
| `mcp.go` | `/api/mcp/client/{id}` | POST/PUT/DELETE MCP client |
|
||||||
|
| `logging.go` | `/api/logs` | GET/DELETE logs |
|
||||||
|
| `logging.go` | `/api/logs/stats` | GET log stats |
|
||||||
|
| `logging.go` | `/api/logs/histogram` | GET log histograms |
|
||||||
|
| `plugins.go` | `/api/plugins` | CRUD plugins |
|
||||||
|
| `config.go` | `/api/config` | GET/PUT config |
|
||||||
|
| `config.go` | `/api/proxy-config` | GET/PUT proxy config |
|
||||||
|
| `cache.go` | `/api/cache/clear/{requestId}` | DELETE cache |
|
||||||
|
| `session.go` | `/api/session/*` | Login/logout/auth check |
|
||||||
|
| `oauth2.go` | `/api/oauth/*` | OAuth callback/status |
|
||||||
|
|
||||||
|
**What to extract from backend research:**
|
||||||
|
- All API endpoints (method, path, description)
|
||||||
|
- Request body fields with types and validation rules
|
||||||
|
- Response body structure
|
||||||
|
- Error responses and status codes
|
||||||
|
- Business logic (what happens when an entity is created/updated)
|
||||||
|
|
||||||
|
### 2c. Read the Config Schema
|
||||||
|
|
||||||
|
The config schema at `transports/config.schema.json` (~2709 lines) is the source of truth for all `config.json` examples.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Extract a specific section from the schema
|
||||||
|
cat transports/config.schema.json | python3 -c "
|
||||||
|
import json, sys
|
||||||
|
schema = json.load(sys.stdin)
|
||||||
|
# For top-level properties:
|
||||||
|
section = schema['properties'].get('<section_name>', {})
|
||||||
|
print(json.dumps(section, indent=2))
|
||||||
|
"
|
||||||
|
|
||||||
|
# For $defs references:
|
||||||
|
cat transports/config.schema.json | python3 -c "
|
||||||
|
import json, sys
|
||||||
|
schema = json.load(sys.stdin)
|
||||||
|
defn = schema.get('\$defs', {}).get('<def_name>', {})
|
||||||
|
print(json.dumps(defn, indent=2))
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Top-level config schema properties:**
|
||||||
|
- `encryption_key` - Encryption key configuration
|
||||||
|
- `auth_config` - Authentication configuration
|
||||||
|
- `client` - Client settings (logging, governance, CORS, etc.)
|
||||||
|
- `framework` - Framework configuration (pricing)
|
||||||
|
- `providers` - Provider configurations (per-provider with keys)
|
||||||
|
- `governance` - Governance (budgets, rate_limits, customers, teams, virtual_keys, routing_rules)
|
||||||
|
- `mcp` - MCP client configs and tool manager
|
||||||
|
- `vector_store` - Vector store backends (weaviate, redis, qdrant, pinecone)
|
||||||
|
- `config_store` - Config store backend (file, postgres)
|
||||||
|
- `logs_store` - Log store backend (file, postgres)
|
||||||
|
- `cluster_config` - Cluster/multinode configuration
|
||||||
|
- `scim_config` - SCIM/SSO configuration
|
||||||
|
- `load_balancer_config` - Adaptive load balancer
|
||||||
|
- `guardrails_config` - Guardrails configuration
|
||||||
|
- `plugins` - Plugin configurations
|
||||||
|
- `audit_logs` - Audit log configuration
|
||||||
|
|
||||||
|
**Key $defs (reusable types):**
|
||||||
|
- `routing_rule` - Routing rule definition
|
||||||
|
- `virtual_key_provider_config` - Provider config within a virtual key
|
||||||
|
- `virtual_key_mcp_config` - MCP config within a virtual key
|
||||||
|
- `provider` / `provider_with_bedrock_config` / `provider_with_azure_config` / `provider_with_vertex_config` - Provider definitions
|
||||||
|
- `base_key` / `bedrock_key` / `azure_key` / `vertex_key` - Key definitions
|
||||||
|
- `mcp_client_config` / `mcp_tool_manager_config` - MCP configs
|
||||||
|
- `weaviate_config` / `redis_config` / `qdrant_config` / `pinecone_config` - Vector store configs
|
||||||
|
- `proxy_config` - Proxy configuration
|
||||||
|
- `cluster_config` / `scim_config` / `load_balancer_config` / `guardrails_config` - Enterprise configs
|
||||||
|
- `pricing_config` / `network_config` / `concurrency_config` - Client sub-configs
|
||||||
|
- `audit_logs_config` - Audit logs config
|
||||||
|
|
||||||
|
**CRITICAL RULE:** Every `config.json` example in documentation MUST be validated against this schema. Extract the relevant section, verify field names, types, required fields, and allowed values before including in the doc.
|
||||||
|
|
||||||
|
### 2d. Check Existing Related Docs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find all docs that mention this feature
|
||||||
|
grep -rn '<feature>' docs/ --include='*.mdx' -l
|
||||||
|
|
||||||
|
# Read the most relevant existing doc
|
||||||
|
cat docs/features/<category>/<feature>.mdx
|
||||||
|
|
||||||
|
# Check cross-references
|
||||||
|
grep -rn '<feature>' docs/ --include='*.mdx' | grep -i 'link\|href\|](/\|see\|read more'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2e. Check OpenAPI Spec
|
||||||
|
|
||||||
|
If the feature has management API endpoints, check if they are in the OpenAPI spec:
|
||||||
|
```bash
|
||||||
|
ls docs/openapi/paths/management/
|
||||||
|
cat docs/openapi/paths/management/<relevant>.yaml 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Research External Context
|
||||||
|
|
||||||
|
### 3a. Use Context7 for Library Documentation
|
||||||
|
|
||||||
|
If the feature involves external libraries or protocols:
|
||||||
|
|
||||||
|
1. Resolve the library ID:
|
||||||
|
```
|
||||||
|
mcp__context7__resolve-library-id with the library name
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Query relevant documentation:
|
||||||
|
```
|
||||||
|
mcp__context7__query-docs with the resolved library ID
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common libraries to research:**
|
||||||
|
- `mintlify` -- For MDX component syntax (Tabs, Info, Note, etc.)
|
||||||
|
- `mark3labs/mcp-go` -- For MCP-related features
|
||||||
|
- `react` -- For UI architecture context
|
||||||
|
- Provider SDKs -- For provider-specific features
|
||||||
|
|
||||||
|
### 3b. Use WebSearch for Additional Context
|
||||||
|
|
||||||
|
Search for:
|
||||||
|
- Official documentation of providers or protocols being documented
|
||||||
|
- Best practices for the feature pattern (e.g., "API gateway rate limiting best practices")
|
||||||
|
- Related open-source project documentation for comparison
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Ask Clarifying Questions
|
||||||
|
|
||||||
|
**ALWAYS ask clarifying questions before writing.** Present what you have learned and ask:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Documentation Plan for: <Feature Name>
|
||||||
|
|
||||||
|
### What I Found
|
||||||
|
- **UI pages:** <list of discovered UI pages and their purpose>
|
||||||
|
- **API endpoints:** <list of relevant endpoints>
|
||||||
|
- **Config schema fields:** <list of relevant config fields>
|
||||||
|
- **Existing docs:** <list of related docs that already exist>
|
||||||
|
|
||||||
|
### Questions
|
||||||
|
1. **Audience:** Is this doc for self-hosted OSS users, enterprise users, or both?
|
||||||
|
2. **Scope:** Should I cover <list specific sub-features>? Anything to exclude?
|
||||||
|
3. **Placement:** I plan to place this at `docs/<path>`. Does that look right?
|
||||||
|
4. **Tab coverage:** Should I include all three config methods (Web UI / API / config.json)?
|
||||||
|
5. **Related features:** Should I cross-reference <related features>?
|
||||||
|
6. <Any feature-specific questions based on ambiguities found during research>
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait for user responses before proceeding.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Present Doc Outline
|
||||||
|
|
||||||
|
After clarifying questions are answered, present a structured outline:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Proposed Outline: <Doc Title>
|
||||||
|
|
||||||
|
**File:** `docs/<path>/<filename>.mdx`
|
||||||
|
**Navigation:** Will be added to `docs.json` under <group> > <subgroup>
|
||||||
|
|
||||||
|
### Frontmatter
|
||||||
|
- title: "<Title>"
|
||||||
|
- description: "<Description>"
|
||||||
|
- icon: "<icon-name>"
|
||||||
|
|
||||||
|
### Sections
|
||||||
|
1. **Overview** - What the feature does, key benefits, core concepts
|
||||||
|
2. **How It Works** - Technical explanation with flow diagram
|
||||||
|
3. **Configuration** (Tabs: Web UI / API / config.json)
|
||||||
|
- Web UI steps with screenshot placeholders
|
||||||
|
- API examples with curl commands
|
||||||
|
- config.json examples (validated against schema)
|
||||||
|
4. **<Feature-specific sections>** - E.g., "Budget Hierarchy", "Rate Limiting", etc.
|
||||||
|
5. **Examples** - Real-world use cases with complete configs
|
||||||
|
6. **Troubleshooting** - Common issues and solutions
|
||||||
|
7. **Next Steps** - Links to related docs
|
||||||
|
|
||||||
|
### Screenshots Needed
|
||||||
|
- `` -- <what to capture>
|
||||||
|
- ...
|
||||||
|
|
||||||
|
### Cross-References
|
||||||
|
- [<Related Doc 1>](<path>)
|
||||||
|
- [<Related Doc 2>](<path>)
|
||||||
|
|
||||||
|
**Proceed with writing?** (yes / no / modify outline)
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait for approval before writing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6: Write the Documentation
|
||||||
|
|
||||||
|
### 6a. MDX File Structure
|
||||||
|
|
||||||
|
Every doc file follows this structure:
|
||||||
|
|
||||||
|
```mdx
|
||||||
|
---
|
||||||
|
title: "<Page Title>"
|
||||||
|
description: "<Short description for SEO and navigation>"
|
||||||
|
icon: "<fontawesome-icon-name>"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
<1-3 paragraphs explaining what the feature does and why it matters>
|
||||||
|
|
||||||
|
**Key Benefits/Features:**
|
||||||
|
- **Benefit 1** - Description
|
||||||
|
- **Benefit 2** - Description
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## <Core Concept Section>
|
||||||
|
|
||||||
|
<Explain the core concepts, architecture, or flow>
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[Request] --> B{Check}
|
||||||
|
B --> C[Result]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
<Tabs group="config-method">
|
||||||
|
<Tab title="Web UI">
|
||||||
|
|
||||||
|
1. Step-by-step instructions
|
||||||
|
2. With numbered steps
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Continue steps after screenshot
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
<Tab title="API">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/<endpoint> \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"field": "value"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Success",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
<Tab title="config.json">
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"<section>": {
|
||||||
|
"<field>": "<value>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `field` | string | Yes | Description |
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## <Additional Sections>
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
Important information the user should know.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Caveats or edge cases to be aware of.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
Dangerous or irreversible operations.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issue 1
|
||||||
|
**Symptom:** ...
|
||||||
|
**Cause:** ...
|
||||||
|
**Fix:** ...
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- **[Related Feature 1](../path)** - Brief description
|
||||||
|
- **[Related Feature 2](../path)** - Brief description
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6b. Screenshot Placeholder Rules
|
||||||
|
|
||||||
|
**Naming convention:** `ui-<feature>-<element>.png`
|
||||||
|
|
||||||
|
Examples from existing docs:
|
||||||
|
- `ui-virtual-key.png` - Virtual key creation form
|
||||||
|
- `ui-virtual-key-routing.png` - Virtual key routing configuration
|
||||||
|
- `ui-virtual-key-provider-config.png` - Provider config within VK
|
||||||
|
- `ui-create-teams.png` - Team creation form
|
||||||
|
- `ui-create-customer.png` - Customer creation form
|
||||||
|
- `ui-config.png` - General config page
|
||||||
|
- `ui-mcp-servers-table.png` - MCP servers listing
|
||||||
|
- `ui-mcp-new-server.png` - New MCP server form
|
||||||
|
- `ui-routing-rules-dashboard.png` - Routing rules overview
|
||||||
|
- `ui-semantic-cache-config.png` - Semantic cache settings
|
||||||
|
- `ui-tracing-config.png` - Tracing configuration
|
||||||
|
|
||||||
|
**Path in docs:** Always use relative path from the doc file location:
|
||||||
|
- From `docs/features/governance/*.mdx`: `../../media/ui-<name>.png`
|
||||||
|
- From `docs/features/*.mdx`: `../media/ui-<name>.png`
|
||||||
|
- From `docs/enterprise/*.mdx`: `../media/ui-<name>.png`
|
||||||
|
- From `docs/mcp/*.mdx`: `../media/ui-<name>.png`
|
||||||
|
- From `docs/providers/*.mdx`: `../media/ui-<name>.png`
|
||||||
|
- From `docs/quickstart/gateway/*.mdx`: `../../media/ui-<name>.png`
|
||||||
|
|
||||||
|
**Format:**
|
||||||
|
```markdown
|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
- Alt text should describe what the screenshot shows (e.g., "Virtual Key Provider Configuration Interface")
|
||||||
|
- File name should be lowercase, hyphen-separated
|
||||||
|
- One screenshot per major UI interaction (creation form, table view, config panel, etc.)
|
||||||
|
- Place screenshot AFTER the step that leads to that UI state, not before
|
||||||
|
- Check if the screenshot already exists in `docs/media/` before creating a new placeholder
|
||||||
|
|
||||||
|
### 6c. Config.json Example Validation
|
||||||
|
|
||||||
|
**MANDATORY:** Before writing any config.json example:
|
||||||
|
|
||||||
|
1. Read the relevant schema section:
|
||||||
|
```bash
|
||||||
|
cat transports/config.schema.json | python3 -c "
|
||||||
|
import json, sys
|
||||||
|
schema = json.load(sys.stdin)
|
||||||
|
section = schema['properties']['<top_level_key>']
|
||||||
|
print(json.dumps(section, indent=2))
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. For $ref references, resolve them:
|
||||||
|
```bash
|
||||||
|
cat transports/config.schema.json | python3 -c "
|
||||||
|
import json, sys
|
||||||
|
schema = json.load(sys.stdin)
|
||||||
|
ref = schema['\$defs']['<def_name>']
|
||||||
|
print(json.dumps(ref, indent=2))
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Verify your example includes:
|
||||||
|
- All `required` fields
|
||||||
|
- Correct field types (string, integer, number, boolean, array, object)
|
||||||
|
- Valid enum values where applicable
|
||||||
|
- Proper nesting structure
|
||||||
|
- Realistic example values (not just "string" or 0)
|
||||||
|
|
||||||
|
4. Cross-check field names against the Go handler request types:
|
||||||
|
```bash
|
||||||
|
grep -A 30 'type Create.*Request\|type Update.*Request' transports/bifrost-http/handlers/<handler>.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6d. Mintlify Component Reference
|
||||||
|
|
||||||
|
**Callout boxes:**
|
||||||
|
```mdx
|
||||||
|
<Info>Informational content - tips, best practices, important context</Info>
|
||||||
|
<Note>Caveats, edge cases, things to be aware of</Note>
|
||||||
|
<Warning>Dangerous operations, irreversible actions, breaking changes</Warning>
|
||||||
|
<Tip>Helpful shortcuts or pro tips</Tip>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tabs (for multi-method configuration):**
|
||||||
|
```mdx
|
||||||
|
<Tabs group="config-method">
|
||||||
|
<Tab title="Web UI">
|
||||||
|
Content for Web UI method
|
||||||
|
</Tab>
|
||||||
|
<Tab title="API">
|
||||||
|
Content for API method
|
||||||
|
</Tab>
|
||||||
|
<Tab title="config.json">
|
||||||
|
Content for config.json method
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `group` attribute ensures tab selection persists across the page. Use consistent group names:
|
||||||
|
- `config-method` - For Web UI / API / config.json tabs
|
||||||
|
- `sdk` or `gateway` - For Gateway / Go SDK tabs
|
||||||
|
- Feature-specific group names for feature-specific tabs
|
||||||
|
|
||||||
|
**Code blocks:**
|
||||||
|
````mdx
|
||||||
|
```bash
|
||||||
|
# Shell commands
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"config": "example"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Go code
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Python code
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// TypeScript code
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A --> B
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
**Tables:**
|
||||||
|
```mdx
|
||||||
|
| Column 1 | Column 2 | Column 3 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| Value 1 | Value 2 | Value 3 |
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6e. Cross-Reference Conventions
|
||||||
|
|
||||||
|
Use relative links for internal cross-references:
|
||||||
|
```mdx
|
||||||
|
[Link Text](./sibling-page)
|
||||||
|
[Link Text](../parent-dir/page)
|
||||||
|
[Link Text](/absolute/from/docs/root)
|
||||||
|
[Link Text](./page#section-anchor)
|
||||||
|
```
|
||||||
|
|
||||||
|
Common cross-reference patterns in Bifrost docs:
|
||||||
|
- Governance docs link to each other (virtual-keys, routing, budget-and-limits, mcp-tools)
|
||||||
|
- Feature docs link to the quickstart guides
|
||||||
|
- Enterprise docs link to the OSS equivalents
|
||||||
|
- All config docs link to the architecture pages
|
||||||
|
- Provider docs link to the supported providers overview
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 7: Update Navigation
|
||||||
|
|
||||||
|
After writing the doc, add it to `docs/docs.json` in the appropriate location.
|
||||||
|
|
||||||
|
### Determining Placement
|
||||||
|
|
||||||
|
The navigation structure in `docs.json` follows this hierarchy:
|
||||||
|
|
||||||
|
```
|
||||||
|
tabs (Documentation, Developer Guides, Deployment Guides, API Reference, Architecture, Benchmarks, Changelogs)
|
||||||
|
└── groups
|
||||||
|
└── pages (strings or nested groups)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Placement rules:**
|
||||||
|
- OSS features go under: `Documentation > Open Source Features`
|
||||||
|
- Enterprise features go under: `Documentation > Enterprise Features`
|
||||||
|
- Provider-specific guides go under: `Documentation > Providers & Guides`
|
||||||
|
- MCP features go under: `Documentation > MCP Gateway`
|
||||||
|
- Architecture docs go under: `Architecture` tab
|
||||||
|
- Deployment guides go under: `Deployment Guides` tab
|
||||||
|
|
||||||
|
**Example: Adding a new governance feature doc:**
|
||||||
|
Find the governance group in docs.json:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"group": "Governance",
|
||||||
|
"icon": "user-lock",
|
||||||
|
"pages": [
|
||||||
|
"features/governance/virtual-keys",
|
||||||
|
"features/governance/routing",
|
||||||
|
"features/governance/budget-and-limits",
|
||||||
|
"features/governance/mcp-tools",
|
||||||
|
"features/governance/NEW-PAGE-HERE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Page path format:** The path in docs.json is relative to `docs/` and omits the `.mdx` extension.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 8: Present for Review
|
||||||
|
|
||||||
|
After writing the doc and updating navigation, present a summary:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Documentation Complete: <Feature Name>
|
||||||
|
|
||||||
|
### Files Created/Modified
|
||||||
|
- **Created:** `docs/<path>/<filename>.mdx` (<line count> lines)
|
||||||
|
- **Modified:** `docs/docs.json` (added to <group> navigation)
|
||||||
|
|
||||||
|
### Screenshots Needed
|
||||||
|
The following screenshot placeholders were added. You will need to capture these:
|
||||||
|
1. `docs/media/ui-<feature>-<element>.png` -- <description of what to capture>
|
||||||
|
2. ...
|
||||||
|
|
||||||
|
### Config.json Validation
|
||||||
|
- All config.json examples validated against `transports/config.schema.json`
|
||||||
|
- Fields verified: <list of key fields>
|
||||||
|
- Schema sections referenced: <list>
|
||||||
|
|
||||||
|
### Cross-References Added
|
||||||
|
- Links TO this doc from: <none yet / list>
|
||||||
|
- Links FROM this doc to: <list of referenced pages>
|
||||||
|
|
||||||
|
### Review Checklist
|
||||||
|
- [ ] Frontmatter title and description are accurate
|
||||||
|
- [ ] All three config methods covered (Web UI / API / config.json)
|
||||||
|
- [ ] config.json examples match schema
|
||||||
|
- [ ] Screenshot placeholders have correct paths and descriptive alt text
|
||||||
|
- [ ] Internal links are valid
|
||||||
|
- [ ] Navigation placement in docs.json is correct
|
||||||
|
- [ ] No duplicate content with existing docs
|
||||||
|
|
||||||
|
**Would you like any changes?**
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mandatory Rules
|
||||||
|
|
||||||
|
### Content Rules
|
||||||
|
- **ALWAYS** research the codebase before writing -- never write from assumptions
|
||||||
|
- **ALWAYS** validate config.json examples against `transports/config.schema.json`
|
||||||
|
- **ALWAYS** include all three config methods (Web UI, API, config.json) for configurable features
|
||||||
|
- **ALWAYS** use the established screenshot naming convention (`ui-<feature>-<element>.png`)
|
||||||
|
- **ALWAYS** present an outline and wait for approval before writing
|
||||||
|
- **NEVER** invent API endpoints -- verify them in the handler files
|
||||||
|
- **NEVER** guess config field names -- verify them in the schema
|
||||||
|
- **NEVER** write documentation for features that do not exist in the codebase
|
||||||
|
- **NEVER** copy content from existing docs without adapting it to the specific feature
|
||||||
|
|
||||||
|
### Style Rules
|
||||||
|
- Use sentence case for headings (e.g., "Budget management" not "Budget Management") -- but follow existing doc convention if different
|
||||||
|
- Use `**bold**` for UI element names (e.g., **Virtual Keys**, **Save** button)
|
||||||
|
- Use backticks for code references (field names, values, commands)
|
||||||
|
- Use numbered steps for sequential instructions (1, 2, 3...)
|
||||||
|
- Use bullet points for non-sequential lists
|
||||||
|
- Keep paragraphs short (2-4 sentences max)
|
||||||
|
- Use horizontal rules (`---`) to separate major sections
|
||||||
|
- Use Info/Note/Warning boxes sparingly -- only when the information is genuinely important
|
||||||
|
|
||||||
|
### Technical Rules
|
||||||
|
- API examples should use `http://localhost:8080` as the base URL
|
||||||
|
- API examples should use `curl` with proper flags (`-X`, `-H`, `-d`)
|
||||||
|
- config.json examples should be minimal but complete (include all required fields)
|
||||||
|
- Field description tables should include: Field, Type, Required, Description
|
||||||
|
- Error responses should include the HTTP status code and error body
|
||||||
|
- Reset duration format: `1m`, `5m`, `1h`, `1d`, `1w`, `1M`, `1Y`
|
||||||
|
|
||||||
|
### Navigation Rules
|
||||||
|
- Page paths in docs.json omit the `.mdx` extension
|
||||||
|
- Page paths are relative to the `docs/` directory
|
||||||
|
- New pages should be added at the end of their group unless order matters logically
|
||||||
|
- Group icons use FontAwesome icon names
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Directory Reference
|
||||||
|
|
||||||
|
```
|
||||||
|
bifrost/
|
||||||
|
├── docs/ # Mintlify documentation
|
||||||
|
│ ├── docs.json # Navigation configuration
|
||||||
|
│ ├── openapi/ # OpenAPI spec (auto-generated API reference)
|
||||||
|
│ ├── media/ # Screenshots and images
|
||||||
|
│ │ └── ui-*.png # UI screenshots (naming convention)
|
||||||
|
│ ├── features/ # Feature documentation
|
||||||
|
│ │ ├── governance/ # Virtual keys, routing, budgets, MCP tools
|
||||||
|
│ │ ├── observability/ # Logging, tracing, Prometheus, OTel
|
||||||
|
│ │ └── plugins/ # Mocker, JSON parser
|
||||||
|
│ ├── enterprise/ # Enterprise feature docs
|
||||||
|
│ ├── mcp/ # MCP Gateway documentation
|
||||||
|
│ ├── providers/ # Provider guides and supported providers
|
||||||
|
│ ├── quickstart/ # Getting started guides
|
||||||
|
│ ├── integrations/ # SDK integration guides
|
||||||
|
│ ├── plugins/ # Custom plugin development
|
||||||
|
│ ├── architecture/ # Architecture documentation
|
||||||
|
│ ├── deployment-guides/ # Deployment guides
|
||||||
|
│ ├── contributing/ # Developer contribution guides
|
||||||
|
│ ├── benchmarking/ # Performance benchmarks
|
||||||
|
│ └── changelogs/ # Version changelogs
|
||||||
|
├── ui/ # React + Vite UI application
|
||||||
|
│ └── app/workspace/ # Feature pages
|
||||||
|
│ ├── providers/ # Provider management
|
||||||
|
│ ├── virtual-keys/ # Virtual key management
|
||||||
|
│ ├── routing-rules/ # Routing rules
|
||||||
|
│ ├── logs/ # LLM request logs
|
||||||
|
│ ├── mcp-registry/ # MCP server registry
|
||||||
|
│ ├── mcp-logs/ # MCP tool execution logs
|
||||||
|
│ ├── mcp-tool-groups/ # MCP tool groups
|
||||||
|
│ ├── mcp-auth-config/ # MCP OAuth configuration
|
||||||
|
│ ├── plugins/ # Plugin configuration
|
||||||
|
│ ├── observability/ # Observability settings
|
||||||
|
│ ├── dashboard/ # Analytics dashboard
|
||||||
|
│ ├── config/ # System configuration
|
||||||
|
│ ├── guardrails/ # Guardrails (enterprise)
|
||||||
|
│ ├── adaptive-routing/ # Adaptive load balancing (enterprise)
|
||||||
|
│ ├── cluster/ # Clustering (enterprise)
|
||||||
|
│ ├── rbac/ # RBAC (enterprise)
|
||||||
|
│ ├── scim/ # SCIM provisioning (enterprise)
|
||||||
|
│ ├── user-groups/ # User groups (enterprise)
|
||||||
|
│ ├── audit-logs/ # Audit logs (enterprise)
|
||||||
|
│ ├── model-limits/ # Model limits
|
||||||
|
│ ├── alert-channels/ # Alert channels
|
||||||
|
│ └── prompt-repo/ # Prompt repository
|
||||||
|
├── transports/
|
||||||
|
│ ├── config.schema.json # Configuration schema (source of truth)
|
||||||
|
│ └── bifrost-http/
|
||||||
|
│ └── handlers/ # HTTP handlers
|
||||||
|
│ ├── governance.go # Governance CRUD
|
||||||
|
│ ├── providers.go # Provider CRUD + keys + models
|
||||||
|
│ ├── mcp.go # MCP client management
|
||||||
|
│ ├── logging.go # Log queries
|
||||||
|
│ ├── plugins.go # Plugin CRUD
|
||||||
|
│ ├── config.go # Config get/update
|
||||||
|
│ ├── cache.go # Cache management
|
||||||
|
│ ├── session.go # Session/auth management
|
||||||
|
│ ├── oauth2.go # OAuth callback handling
|
||||||
|
│ ├── inference.go # LLM inference routing
|
||||||
|
│ ├── mcpinference.go # MCP inference
|
||||||
|
│ ├── mcpserver.go # MCP server (SSE/streamable)
|
||||||
|
│ ├── integrations.go # SDK integrations
|
||||||
|
│ ├── health.go # Health check
|
||||||
|
│ └── websocket.go # WebSocket handler
|
||||||
|
├── core/ # Go core library
|
||||||
|
│ ├── schemas/ # All Go types/schemas
|
||||||
|
│ ├── providers/ # Provider implementations
|
||||||
|
│ └── mcp/ # MCP protocol implementation
|
||||||
|
├── framework/ # Framework layer
|
||||||
|
│ ├── configstore/ # Config storage backends
|
||||||
|
│ └── logstore/ # Log storage backends
|
||||||
|
└── plugins/ # Go plugins
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Feature Not Found in Codebase
|
||||||
|
If the feature name does not map to any UI page or handler:
|
||||||
|
1. Search more broadly: `grep -ri "<feature>" ui/ transports/ core/ --include='*.go' --include='*.tsx' -l`
|
||||||
|
2. Ask the user: "I could not find a UI page or API handler for '<feature>'. Can you point me to the relevant code?"
|
||||||
|
3. The feature might be code-only (Go SDK) with no UI -- adjust the doc accordingly (no Web UI tab)
|
||||||
|
|
||||||
|
### Config Schema Section Not Found
|
||||||
|
If the feature does not have a config.json section:
|
||||||
|
1. Some features are API/UI-only (no file-based config)
|
||||||
|
2. Skip the config.json tab in that case
|
||||||
|
3. Note in the doc: "This feature is configured via the Web UI or API only."
|
||||||
|
|
||||||
|
### Existing Doc Conflicts
|
||||||
|
If a doc already exists for the feature:
|
||||||
|
1. Read the existing doc thoroughly
|
||||||
|
2. Ask the user: "A doc already exists at `<path>`. Should I update it or create a new companion page?"
|
||||||
|
3. If updating, make targeted edits rather than rewriting the entire doc
|
||||||
950
.claude/skills/e2e-test/SKILL.md
Normal file
950
.claude/skills/e2e-test/SKILL.md
Normal file
@@ -0,0 +1,950 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
name: e2e-test
|
||||||
|
description: Write, run, debug, audit, and auto-update Playwright E2E tests for the Bifrost UI. Use when asked to create new E2E tests, add test coverage, fix flaky tests, debug failing tests, audit test correctness, update tests after UI changes, or sync tests with modified components. Invoked with /e2e-test <FEATURE_NAME>, /e2e-test fix <SPEC_FILE>, /e2e-test sync, or /e2e-test audit.
|
||||||
|
allowed-tools: Read, Grep, Glob, Bash, Edit, Write, Task, AskUserQuestion, TodoWrite
|
||||||
|
---
|
||||||
|
|
||||||
|
# Playwright E2E Testing
|
||||||
|
|
||||||
|
Write, run, debug, and auto-update Playwright E2E tests following Bifrost's established patterns and conventions. Automatically detects UI changes and updates affected tests.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/e2e-test <FEATURE_NAME> # Create or update tests for a feature
|
||||||
|
/e2e-test fix <SPEC_FILE> # Debug and fix a failing test
|
||||||
|
/e2e-test run <FEATURE_NAME> # Run tests for a specific feature
|
||||||
|
/e2e-test run # Run all E2E tests
|
||||||
|
/e2e-test sync # Detect UI changes and update affected tests
|
||||||
|
/e2e-test sync <FEATURE_NAME> # Sync tests for a specific feature with UI changes
|
||||||
|
/e2e-test audit # Audit all specs for incorrect/weak assertions
|
||||||
|
/e2e-test audit <FEATURE_NAME> # Audit a specific feature's specs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow Overview
|
||||||
|
|
||||||
|
1. **Understand the feature** - Read the UI code to understand what needs testing
|
||||||
|
2. **Check existing tests** - Review existing test patterns for the feature or similar features
|
||||||
|
3. **Identify data-testid attributes** - Find selectors in the UI code
|
||||||
|
4. **Write/update tests** - Follow the established patterns (page objects, data factories, fixtures)
|
||||||
|
5. **Run the tests** - Execute and verify they pass
|
||||||
|
6. **Fix failures** - Debug and fix any issues
|
||||||
|
|
||||||
|
## Auto-Update Workflow (sync mode)
|
||||||
|
|
||||||
|
When invoked with `sync`, or when UI changes are detected, automatically update E2E tests:
|
||||||
|
|
||||||
|
### Step 0: Detect What Changed
|
||||||
|
|
||||||
|
Detect UI changes by checking git diff against the base branch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get all changed UI files
|
||||||
|
git diff main --name-only -- 'ui/'
|
||||||
|
|
||||||
|
# Get changed files with diff content for analysis
|
||||||
|
git diff main -- 'ui/app/workspace/' 'ui/components/'
|
||||||
|
```
|
||||||
|
|
||||||
|
Categorize changes into:
|
||||||
|
- **data-testid changes** - Renamed, removed, or added test IDs
|
||||||
|
- **Route/URL changes** - Modified page routes or navigation paths
|
||||||
|
- **Component structure changes** - New/removed form fields, buttons, tables, dialogs
|
||||||
|
- **API endpoint changes** - Modified API calls that tests rely on
|
||||||
|
- **New features** - Entirely new pages or components that need test coverage
|
||||||
|
|
||||||
|
### Step 1: Map UI Changes to Affected Tests
|
||||||
|
|
||||||
|
For each changed UI file, find the corresponding test files:
|
||||||
|
|
||||||
|
```
|
||||||
|
UI File Path → Test Feature Folder
|
||||||
|
ui/app/workspace/providers/** → tests/e2e/features/providers/
|
||||||
|
ui/app/workspace/virtual-keys/** → tests/e2e/features/virtual-keys/
|
||||||
|
ui/app/workspace/dashboard/** → tests/e2e/features/dashboard/
|
||||||
|
ui/app/workspace/logs/** → tests/e2e/features/logs/
|
||||||
|
ui/app/workspace/mcp-logs/** → tests/e2e/features/mcp-logs/
|
||||||
|
ui/app/workspace/mcp-registry/** → tests/e2e/features/mcp-registry/
|
||||||
|
ui/app/workspace/routing-rules/** → tests/e2e/features/routing-rules/
|
||||||
|
ui/app/workspace/observability/** → tests/e2e/features/observability/
|
||||||
|
ui/app/workspace/config/** → tests/e2e/features/config/
|
||||||
|
ui/app/workspace/plugins/** → tests/e2e/features/plugins/
|
||||||
|
ui/components/sidebar.tsx → tests/e2e/core/pages/sidebar.page.ts
|
||||||
|
ui/components/** → May affect multiple test features
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Analyze Each Change Type and Update
|
||||||
|
|
||||||
|
**A. data-testid renamed or removed:**
|
||||||
|
1. Search the old testid across all test files: `grep -r 'old-testid' tests/e2e/`
|
||||||
|
2. Update every reference in page objects, selectors, and spec files
|
||||||
|
3. If a testid was removed without replacement, check if the element still exists with a different selector and add a new testid to the UI
|
||||||
|
|
||||||
|
**B. Form fields added/removed:**
|
||||||
|
1. If a new form field was added to a create/edit form:
|
||||||
|
- Add the field to the page object's interface (e.g., `FeatureConfig`)
|
||||||
|
- Add a locator for the field in the page object constructor
|
||||||
|
- Update the `createFeature()` / `editFeature()` methods to fill the field
|
||||||
|
- Update the data factory to include the new field with a default value
|
||||||
|
- Add a test case that exercises the new field
|
||||||
|
2. If a form field was removed:
|
||||||
|
- Remove it from the interface, locators, and methods
|
||||||
|
- Remove or update test cases that depended on it
|
||||||
|
|
||||||
|
**C. New buttons/actions added:**
|
||||||
|
1. Add locators to the page object
|
||||||
|
2. Add methods to interact with the new action
|
||||||
|
3. Add test cases covering the new action
|
||||||
|
|
||||||
|
**D. Route changes:**
|
||||||
|
1. Update `goto()` methods in page objects
|
||||||
|
2. Update navigation helpers in `core/actions/navigation.ts`
|
||||||
|
3. Update any hardcoded URLs in test specs
|
||||||
|
|
||||||
|
**E. API endpoint changes:**
|
||||||
|
1. Update `core/actions/api.ts` helpers
|
||||||
|
2. Update any `waitForResponse()` URL patterns in page objects
|
||||||
|
|
||||||
|
**F. New page/feature added:**
|
||||||
|
1. Create the full test structure (page object, data factory, spec, fixture registration)
|
||||||
|
2. Follow Step 3 from the main workflow
|
||||||
|
|
||||||
|
### Step 3: Validate Updates
|
||||||
|
|
||||||
|
After making changes, run the affected tests to verify:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run only the affected feature tests
|
||||||
|
npx playwright test features/<affected-feature> --reporter=list
|
||||||
|
|
||||||
|
# If multiple features affected, run them all
|
||||||
|
npx playwright test features/providers features/virtual-keys --reporter=list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Report Changes
|
||||||
|
|
||||||
|
Present a summary to the user:
|
||||||
|
```
|
||||||
|
## E2E Test Sync Summary
|
||||||
|
|
||||||
|
### UI Changes Detected
|
||||||
|
- <list of changed UI files>
|
||||||
|
|
||||||
|
### Tests Updated
|
||||||
|
- **<feature>.page.ts**: Updated locators for renamed data-testid, added new field method
|
||||||
|
- **<feature>.spec.ts**: Added test for new "export" button, updated form fill sequence
|
||||||
|
- **<feature>.data.ts**: Added new field to factory defaults
|
||||||
|
|
||||||
|
### Tests Created
|
||||||
|
- **<new-feature>/**: Full test suite for new feature (page object, data, spec)
|
||||||
|
|
||||||
|
### Tests Requiring Manual Review
|
||||||
|
- <any changes that couldn't be auto-resolved, e.g., complex interaction flow changes>
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- Ran `npx playwright test features/<feature>` → X passed, Y failed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Important: Proactive Sync Triggers
|
||||||
|
|
||||||
|
**ALWAYS** check for and sync E2E tests when ANY of these happen:
|
||||||
|
|
||||||
|
1. **You modify a UI component** that has `data-testid` attributes — check if tests reference those IDs
|
||||||
|
2. **You add a new `data-testid`** to a component — consider if existing tests should use it
|
||||||
|
3. **You rename or remove a component/prop** — search for test references and update them
|
||||||
|
4. **You change a form's fields** (add/remove inputs, change validation) — update page object methods and test data
|
||||||
|
5. **You modify an API route** that tests call — update API helpers and response wait patterns
|
||||||
|
6. **You change page navigation/routing** — update `goto()` methods and navigation helpers
|
||||||
|
|
||||||
|
To check quickly if tests are affected by your UI change:
|
||||||
|
```bash
|
||||||
|
# Find test files that reference any testid from the changed component
|
||||||
|
grep -rl 'data-testid-from-component' tests/e2e/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit Workflow (audit mode) — Fix Incorrect Specs
|
||||||
|
|
||||||
|
Tests that pass but validate the wrong things are worse than no tests — they give false confidence. When invoked with `audit`, systematically scan specs for correctness issues and fix them.
|
||||||
|
|
||||||
|
### Step 0: Scope the Audit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Audit all specs
|
||||||
|
/e2e-test audit
|
||||||
|
|
||||||
|
# Audit a specific feature
|
||||||
|
/e2e-test audit virtual-keys
|
||||||
|
```
|
||||||
|
|
||||||
|
If a specific feature is given, read only that feature's spec, page object, and data files. Otherwise, scan all `features/**/*.spec.ts` files.
|
||||||
|
|
||||||
|
### Step 1: Read the UI Code First
|
||||||
|
|
||||||
|
For every spec file being audited, **read the actual UI component code** to understand what the UI really does. This is the source of truth — tests must match real behavior, not assumed behavior.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For each feature, read the UI code
|
||||||
|
# e.g. for virtual-keys:
|
||||||
|
grep -r 'data-testid' ui/app/workspace/virtual-keys/ --include='*.tsx'
|
||||||
|
```
|
||||||
|
|
||||||
|
Understand:
|
||||||
|
- What fields are in the create/edit form (from the UI code, not the test)
|
||||||
|
- What the save button actually does (API call, toast, sheet close)
|
||||||
|
- What the table actually renders (columns, row content, empty state)
|
||||||
|
- What validation rules exist (required fields, format checks)
|
||||||
|
- What error states exist (API failures, permission errors)
|
||||||
|
|
||||||
|
### Step 2: Scan for Incorrect Assertion Patterns
|
||||||
|
|
||||||
|
Check each spec file for these **anti-patterns** (ordered by severity):
|
||||||
|
|
||||||
|
#### P0 — Tests That Can Never Fail (always-true assertions)
|
||||||
|
|
||||||
|
These are the most dangerous — they provide zero coverage while appearing green.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// WRONG: Always true — count is always >= 0
|
||||||
|
const count = await page.getCount()
|
||||||
|
expect(count >= 0).toBe(true)
|
||||||
|
|
||||||
|
// WRONG: Always true — empty string is a string
|
||||||
|
const text = await element.textContent()
|
||||||
|
expect(typeof text).toBe('string')
|
||||||
|
|
||||||
|
// WRONG: Always true — isVisible returns a boolean, not asserted correctly
|
||||||
|
const visible = await element.isVisible()
|
||||||
|
// (no assertion at all, or expect(visible).toBeDefined())
|
||||||
|
|
||||||
|
// WRONG: Catching error means it never fails
|
||||||
|
try { await doSomething(); expect(true).toBe(true) } catch { expect(true).toBe(true) }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix:** Replace with deterministic assertions that verify actual expected state.
|
||||||
|
|
||||||
|
#### P1 — Tests That Assert Existence But Not Correctness
|
||||||
|
|
||||||
|
The test creates an item and checks it exists, but never verifies the item has the right data.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// WEAK: Only checks existence, not content
|
||||||
|
await page.createVirtualKey({ name: 'Test VK', description: 'My desc', budget: { maxLimit: 100 } })
|
||||||
|
const exists = await page.virtualKeyExists('Test VK')
|
||||||
|
expect(exists).toBe(true)
|
||||||
|
// Never checks that description, budget, or other fields were actually saved correctly
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix:** After create, open/view the item and verify its fields match what was submitted. Compare against the actual UI state:
|
||||||
|
```typescript
|
||||||
|
// BETTER: Verify the data was saved correctly
|
||||||
|
await page.viewVirtualKey(vkData.name)
|
||||||
|
await expect(page.descriptionInput).toHaveValue('My desc')
|
||||||
|
await expect(page.page.locator('#budgetMaxLimit')).toHaveValue('100')
|
||||||
|
```
|
||||||
|
|
||||||
|
#### P2 — Tests That Assert the Wrong Thing
|
||||||
|
|
||||||
|
The test name says one thing but asserts something unrelated.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// WRONG: Test says "should validate email" but only checks button state
|
||||||
|
test('should validate email format', async ({ page }) => {
|
||||||
|
await page.fillEmail('invalid')
|
||||||
|
await expect(page.saveBtn).toBeDisabled() // Tests button, not validation message
|
||||||
|
})
|
||||||
|
|
||||||
|
// WRONG: Test says "should delete" but only checks toast, not actual deletion
|
||||||
|
test('should delete item', async ({ page }) => {
|
||||||
|
await page.deleteItem('foo')
|
||||||
|
await page.waitForSuccessToast()
|
||||||
|
// Never checks the item is actually gone from the table
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix:** Align assertions with test intent. A delete test must verify the item is gone. A validation test must check the validation message.
|
||||||
|
|
||||||
|
#### P3 — Tests With Swallowed Errors / Catch-All Handlers
|
||||||
|
|
||||||
|
Tests that catch errors and silently continue, hiding real failures.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// WRONG: Error is caught and ignored — test always passes
|
||||||
|
const isVisible = await element.isVisible().catch(() => false)
|
||||||
|
if (isVisible) {
|
||||||
|
expect(isVisible).toBe(true) // Only asserts when visible, silently passes when not
|
||||||
|
}
|
||||||
|
|
||||||
|
// WRONG: Optional assertion that can be skipped entirely
|
||||||
|
const providerSection = page.getByText(/Providers/i).first()
|
||||||
|
const isProviderVisible = await providerSection.isVisible().catch(() => false)
|
||||||
|
if (isProviderVisible) {
|
||||||
|
expect(isProviderVisible).toBe(true) // Tautology when reached, skipped when not
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix:** Remove the catch/conditional and make the assertion unconditional. If the element should be visible, assert it directly:
|
||||||
|
```typescript
|
||||||
|
await expect(element).toBeVisible()
|
||||||
|
```
|
||||||
|
|
||||||
|
If the state genuinely depends on external factors, use count-based branching with **both** branches making meaningful assertions:
|
||||||
|
```typescript
|
||||||
|
const count = await page.getCount()
|
||||||
|
if (count === 0) {
|
||||||
|
await expect(page.emptyState).toBeVisible()
|
||||||
|
} else {
|
||||||
|
expect(count).toBeGreaterThan(0)
|
||||||
|
await expect(page.emptyState).not.toBeVisible()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### P4 — Tests That Don't Assert After Actions
|
||||||
|
|
||||||
|
Tests that perform actions (clicks, form fills, navigation) but have no assertion afterward.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// WRONG: Action with no assertion — test only checks nothing throws
|
||||||
|
test('should toggle visibility', async ({ page }) => {
|
||||||
|
await page.toggleKeyVisibility('my-key')
|
||||||
|
await page.toggleKeyVisibility('my-key')
|
||||||
|
// No assertion that the visibility state actually changed
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix:** Add assertions verifying the action's observable effect:
|
||||||
|
```typescript
|
||||||
|
test('should toggle visibility', async ({ page }) => {
|
||||||
|
await page.toggleKeyVisibility('my-key')
|
||||||
|
// Verify key value is now visible
|
||||||
|
await expect(page.getKeyValueText('my-key')).toBeVisible()
|
||||||
|
|
||||||
|
await page.toggleKeyVisibility('my-key')
|
||||||
|
// Verify key value is now hidden again
|
||||||
|
await expect(page.getKeyValueText('my-key')).not.toBeVisible()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### P5 — Tests Asserting Against Stale State
|
||||||
|
|
||||||
|
Tests that read state before an action completes, or compare to a stale snapshot.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// WRONG: Reads count before table finishes refreshing
|
||||||
|
await page.deleteItem('foo')
|
||||||
|
const count = await page.getCount() // Table hasn't refreshed yet!
|
||||||
|
expect(count).toBe(previousCount - 1) // May pass by coincidence
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix:** Wait for the state change to complete before asserting:
|
||||||
|
```typescript
|
||||||
|
await page.deleteItem('foo')
|
||||||
|
await page.waitForItemGone('foo') // Wait for table refresh
|
||||||
|
const count = await page.getCount()
|
||||||
|
expect(count).toBe(previousCount - 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### P6 — Tests That Duplicate Other Tests Without Additional Value
|
||||||
|
|
||||||
|
Multiple tests covering the exact same code path with only cosmetic differences (different name strings, same logic).
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// REDUNDANT: These three tests do the same thing with different budget values
|
||||||
|
test('should create VK with small budget', ...) // creates + checks exists
|
||||||
|
test('should create VK with medium budget', ...) // creates + checks exists
|
||||||
|
test('should create VK with daily budget', ...) // creates + checks exists
|
||||||
|
// None of them verify the budget value was saved correctly
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix:** Either consolidate into a parameterized test, or make each test verify something unique (e.g., verify the actual budget value appears in the UI).
|
||||||
|
|
||||||
|
### Step 3: Cross-Check Against UI Code
|
||||||
|
|
||||||
|
For each test, verify:
|
||||||
|
|
||||||
|
1. **Form fields match** - Does the test fill all required fields from the UI? Does it test fields that actually exist?
|
||||||
|
2. **Selectors are correct** - Does `data-testid="vk-name-input"` actually exist in the current UI code? Has it been renamed?
|
||||||
|
3. **Behavior matches** - If the UI shows a confirmation dialog on delete, does the test handle it? If the form has validation, do tests exercise it?
|
||||||
|
4. **Error paths exist** - Does the UI show error toasts? Are there tests for error scenarios?
|
||||||
|
5. **New UI capabilities untested** - Has the UI added new features (export, filter, sort, pagination) that have no test coverage?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Compare what testids exist in UI vs what tests reference
|
||||||
|
grep -roh 'data-testid="[^"]*"' ui/app/workspace/<feature>/ | sort -u > /tmp/ui-testids.txt
|
||||||
|
grep -roh "getByTestId('[^']*')\|getByTestId(\"[^\"]*\")" tests/e2e/features/<feature>/ | sort -u > /tmp/test-testids.txt
|
||||||
|
# Diff to find gaps
|
||||||
|
diff /tmp/ui-testids.txt /tmp/test-testids.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Fix and Strengthen
|
||||||
|
|
||||||
|
For each issue found:
|
||||||
|
|
||||||
|
1. **Read the UI code** for that specific component/interaction
|
||||||
|
2. **Understand the expected behavior** from the UI implementation
|
||||||
|
3. **Rewrite the assertion** to verify actual, observable, correct state
|
||||||
|
4. **Run the fixed test** to ensure it passes with correct behavior and **would fail** if the behavior broke
|
||||||
|
|
||||||
|
### Step 5: Report Findings
|
||||||
|
|
||||||
|
Present results to the user:
|
||||||
|
|
||||||
|
```
|
||||||
|
## E2E Audit Report — <feature>
|
||||||
|
|
||||||
|
### Issues Found: X
|
||||||
|
|
||||||
|
#### P0 — Always-True Assertions (X found)
|
||||||
|
| Test | File:Line | Issue | Fix |
|
||||||
|
|------|-----------|-------|-----|
|
||||||
|
| "should show empty state" | vk.spec.ts:374 | `count >= 0` always true | Use count-based branching |
|
||||||
|
|
||||||
|
#### P1 — Missing Data Verification (X found)
|
||||||
|
| Test | File:Line | Issue | Fix |
|
||||||
|
|------|-----------|-------|-----|
|
||||||
|
| "should create with budget" | vk.spec.ts:108 | Only checks `exists`, not budget value | Add field verification after create |
|
||||||
|
|
||||||
|
#### P2 — Wrong Assertion Target (X found)
|
||||||
|
...
|
||||||
|
|
||||||
|
#### P3 — Swallowed Errors (X found)
|
||||||
|
...
|
||||||
|
|
||||||
|
#### P4 — Missing Assertions (X found)
|
||||||
|
...
|
||||||
|
|
||||||
|
#### P5 — Stale State (X found)
|
||||||
|
...
|
||||||
|
|
||||||
|
#### P6 — Redundant Tests (X found)
|
||||||
|
...
|
||||||
|
|
||||||
|
### Coverage Gaps
|
||||||
|
- No test for: <UI feature that exists but has no test>
|
||||||
|
- Missing error path test for: <error scenario>
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
- X tests fixed
|
||||||
|
- X tests need manual review (complex interaction flows)
|
||||||
|
- X new tests added for coverage gaps
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
All E2E tests live in `tests/e2e/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/e2e/
|
||||||
|
├── playwright.config.ts # Playwright configuration
|
||||||
|
├── global-setup.ts # Global setup (plugin build, MCP servers, Bifrost connectivity)
|
||||||
|
├── core/ # Shared utilities & fixtures
|
||||||
|
│ ├── fixtures/
|
||||||
|
│ │ ├── base.fixture.ts # Main fixture - exports `test` and `expect` with all page objects
|
||||||
|
│ │ └── test-data.fixture.ts # TestDataFactory for generating unique test data
|
||||||
|
│ ├── pages/
|
||||||
|
│ │ ├── base.page.ts # BasePage class with common methods (toasts, forms, waits)
|
||||||
|
│ │ └── sidebar.page.ts # Sidebar navigation
|
||||||
|
│ ├── actions/
|
||||||
|
│ │ ├── navigation.ts # Navigation helpers (goToProviders, goToVirtualKeys, etc.)
|
||||||
|
│ │ └── api.ts # API helpers for setup/cleanup (providersApi, virtualKeysApi, etc.)
|
||||||
|
│ └── utils/
|
||||||
|
│ ├── selectors.ts # Centralized selector definitions
|
||||||
|
│ └── test-helpers.ts # Utilities: waitForNetworkIdle, retry, fillSelect, assertToast, etc.
|
||||||
|
└── features/ # One folder per feature
|
||||||
|
└── <feature>/
|
||||||
|
├── <feature>.spec.ts # Test cases
|
||||||
|
├── <feature>.data.ts # Test data factories & sample constants
|
||||||
|
└── pages/
|
||||||
|
└── <feature>.page.ts # Page object extending BasePage
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 1: Understand the Feature
|
||||||
|
|
||||||
|
Before writing tests, read the relevant UI code to understand:
|
||||||
|
|
||||||
|
- What pages/routes exist for the feature
|
||||||
|
- What `data-testid` attributes are already in the UI components
|
||||||
|
- What CRUD operations are available
|
||||||
|
- What form fields, buttons, and interactive elements exist
|
||||||
|
- What API endpoints the UI calls
|
||||||
|
|
||||||
|
**Search for data-testid in UI code:**
|
||||||
|
```bash
|
||||||
|
# Find all data-testid attributes for a feature
|
||||||
|
grep -r 'data-testid' ui/app/workspace/<feature>/ --include='*.tsx' --include='*.ts'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check what routes exist:**
|
||||||
|
```bash
|
||||||
|
ls ui/app/workspace/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Check Existing Tests
|
||||||
|
|
||||||
|
Always review existing patterns before writing new tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all existing feature test folders
|
||||||
|
ls tests/e2e/features/
|
||||||
|
|
||||||
|
# Read an existing spec for patterns
|
||||||
|
cat tests/e2e/features/virtual-keys/virtual-keys.spec.ts
|
||||||
|
|
||||||
|
# Read existing page objects for patterns
|
||||||
|
cat tests/e2e/features/virtual-keys/pages/virtual-keys.page.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Create the Feature Test Structure
|
||||||
|
|
||||||
|
For a new feature, create these files:
|
||||||
|
|
||||||
|
### 3a. Page Object (`features/<feature>/pages/<feature>.page.ts`)
|
||||||
|
|
||||||
|
**CRITICAL RULES:**
|
||||||
|
- Always extend `BasePage`
|
||||||
|
- Define all locators in the constructor using `page.getByTestId()`
|
||||||
|
- Use `data-testid` attributes as the primary selector strategy
|
||||||
|
- Use `page.getByRole()` as a secondary strategy
|
||||||
|
- NEVER use brittle CSS selectors or chained parent locators (`.locator('..')`)
|
||||||
|
- Methods should be async and use semantic waits
|
||||||
|
- Include `goto()`, CRUD methods, and a `cleanup` method
|
||||||
|
|
||||||
|
**Template:**
|
||||||
|
```typescript
|
||||||
|
import { Locator, Page, expect } from '@playwright/test'
|
||||||
|
import { BasePage } from '../../../core/pages/base.page'
|
||||||
|
import { waitForNetworkIdle } from '../../../core/utils/test-helpers'
|
||||||
|
|
||||||
|
// Define interfaces for the feature's data
|
||||||
|
export interface FeatureConfig {
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
// ... other fields
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FeaturePage extends BasePage {
|
||||||
|
// Main page elements
|
||||||
|
readonly createBtn: Locator
|
||||||
|
readonly table: Locator
|
||||||
|
|
||||||
|
// Sheet/form elements
|
||||||
|
readonly sheet: Locator
|
||||||
|
readonly nameInput: Locator
|
||||||
|
readonly saveBtn: Locator
|
||||||
|
readonly cancelBtn: Locator
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
super(page)
|
||||||
|
|
||||||
|
// Use getByTestId for all locators
|
||||||
|
this.createBtn = page.getByTestId('create-feature-btn')
|
||||||
|
this.table = page.getByTestId('feature-table')
|
||||||
|
this.sheet = page.getByTestId('feature-sheet')
|
||||||
|
this.nameInput = page.getByTestId('feature-name-input')
|
||||||
|
this.saveBtn = page.getByTestId('feature-save-btn')
|
||||||
|
this.cancelBtn = page.getByTestId('feature-cancel-btn')
|
||||||
|
}
|
||||||
|
|
||||||
|
async goto(): Promise<void> {
|
||||||
|
await this.page.goto('/workspace/<feature>')
|
||||||
|
await waitForNetworkIdle(this.page)
|
||||||
|
}
|
||||||
|
|
||||||
|
async createFeature(config: FeatureConfig): Promise<void> {
|
||||||
|
await this.createBtn.click()
|
||||||
|
await expect(this.sheet).toBeVisible()
|
||||||
|
await this.waitForSheetAnimation()
|
||||||
|
|
||||||
|
// Fill form fields
|
||||||
|
await this.nameInput.fill(config.name)
|
||||||
|
|
||||||
|
// Save
|
||||||
|
await this.saveBtn.click()
|
||||||
|
await this.waitForSuccessToast()
|
||||||
|
await this.dismissToasts()
|
||||||
|
await expect(this.sheet).not.toBeVisible({ timeout: 5000 })
|
||||||
|
}
|
||||||
|
|
||||||
|
async featureExists(name: string): Promise<boolean> {
|
||||||
|
const row = this.page.getByTestId(`feature-row-${name}`)
|
||||||
|
return (await row.count()) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFeature(name: string): Promise<void> {
|
||||||
|
const deleteBtn = this.page.getByTestId(`feature-delete-btn-${name}`)
|
||||||
|
await deleteBtn.click()
|
||||||
|
|
||||||
|
// Handle confirmation dialog
|
||||||
|
const confirmDialog = this.page.locator('[role="alertdialog"]')
|
||||||
|
await confirmDialog.waitFor({ state: 'visible', timeout: 5000 })
|
||||||
|
const confirmBtn = confirmDialog.getByRole('button', { name: /Delete/i })
|
||||||
|
await confirmBtn.click()
|
||||||
|
|
||||||
|
await this.waitForSuccessToast()
|
||||||
|
await this.dismissToasts()
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeSheet(): Promise<void> {
|
||||||
|
const isSheetVisible = await this.sheet.isVisible().catch(() => false)
|
||||||
|
if (isSheetVisible) {
|
||||||
|
const closeBtn = this.sheet.locator('button[aria-label*="close"], button:has(svg.lucide-x)').first()
|
||||||
|
if (await closeBtn.isVisible()) {
|
||||||
|
await closeBtn.click()
|
||||||
|
}
|
||||||
|
await expect(this.sheet).not.toBeVisible({ timeout: 5000 }).catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanupFeatures(names: string[]): Promise<void> {
|
||||||
|
if (names.length === 0) return
|
||||||
|
await this.goto()
|
||||||
|
await this.closeSheet()
|
||||||
|
await this.dismissToasts()
|
||||||
|
for (const name of names) {
|
||||||
|
try {
|
||||||
|
const exists = await this.featureExists(name)
|
||||||
|
if (!exists) continue
|
||||||
|
await this.closeSheet()
|
||||||
|
await this.deleteFeature(name)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[CLEANUP] Failed to delete: ${name}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3b. Test Data Factory (`features/<feature>/<feature>.data.ts`)
|
||||||
|
|
||||||
|
**CRITICAL RULES:**
|
||||||
|
- Use `Date.now()` for unique test names
|
||||||
|
- Provide factory functions with sensible defaults
|
||||||
|
- Allow partial overrides via the spread pattern
|
||||||
|
- Create sample constant objects for reusable configurations
|
||||||
|
- **Never marshal payloads to a `Record`/`Map` and re-serialize** — field ordering matters for backend validation and snapshot comparisons. Always construct payloads as object literals with fields in the intended order. Do NOT use `Object.fromEntries()`, `JSON.parse(JSON.stringify(...))` round-trips, or destructure into an intermediate `Record<string, unknown>` — these can reorder fields.
|
||||||
|
|
||||||
|
**Template:**
|
||||||
|
```typescript
|
||||||
|
import { FeatureConfig } from './pages/feature.page'
|
||||||
|
|
||||||
|
export function createFeatureData(overrides: Partial<FeatureConfig> = {}): FeatureConfig {
|
||||||
|
const timestamp = Date.now()
|
||||||
|
return {
|
||||||
|
name: `Test Feature ${timestamp}`,
|
||||||
|
description: 'E2E test feature',
|
||||||
|
// ... sensible defaults
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample configurations for different scenarios
|
||||||
|
export const SAMPLE_CONFIGS = {
|
||||||
|
basic: { /* ... */ },
|
||||||
|
advanced: { /* ... */ },
|
||||||
|
} as const
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3c. Test Spec (`features/<feature>/<feature>.spec.ts`)
|
||||||
|
|
||||||
|
**CRITICAL RULES:**
|
||||||
|
- Import `test` and `expect` from `../../core/fixtures/base.fixture`
|
||||||
|
- Track created resources in arrays for cleanup in `afterEach`
|
||||||
|
- Use `test.describe()` blocks for logical grouping
|
||||||
|
- Use `test.beforeEach()` to navigate to the page
|
||||||
|
- Use `test.afterEach()` to clean up resources
|
||||||
|
- Use unique names with `Date.now()` for test data
|
||||||
|
- Write deterministic assertions (never `expect(count >= 0).toBe(true)`)
|
||||||
|
- Use `test.describe.configure({ mode: 'serial' })` when tests have write ordering dependencies
|
||||||
|
- **Never marshal API payloads to a `Record`/`Map`** — pass object literals directly to Playwright's `request.post({ data })`. Marshaling through an intermediate map can reorder fields, which breaks backend validation and snapshot comparisons.
|
||||||
|
|
||||||
|
**Template:**
|
||||||
|
```typescript
|
||||||
|
import { expect, test } from '../../core/fixtures/base.fixture'
|
||||||
|
import { createFeatureData } from './feature.data'
|
||||||
|
|
||||||
|
const createdItems: string[] = []
|
||||||
|
|
||||||
|
test.describe('Feature Name', () => {
|
||||||
|
test.beforeEach(async ({ featurePage }) => {
|
||||||
|
await featurePage.goto()
|
||||||
|
})
|
||||||
|
|
||||||
|
test.afterEach(async ({ featurePage }) => {
|
||||||
|
await featurePage.closeSheet()
|
||||||
|
if (createdItems.length > 0) {
|
||||||
|
await featurePage.cleanupFeatures([...createdItems])
|
||||||
|
createdItems.length = 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('Creation', () => {
|
||||||
|
test('should display create button', async ({ featurePage }) => {
|
||||||
|
await expect(featurePage.createBtn).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should create a basic item', async ({ featurePage }) => {
|
||||||
|
const data = createFeatureData({ name: `Basic Test ${Date.now()}` })
|
||||||
|
createdItems.push(data.name)
|
||||||
|
|
||||||
|
await featurePage.createFeature(data)
|
||||||
|
|
||||||
|
const exists = await featurePage.featureExists(data.name)
|
||||||
|
expect(exists).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('Deletion', () => {
|
||||||
|
test('should delete item', async ({ featurePage }) => {
|
||||||
|
const data = createFeatureData({ name: `Delete Test ${Date.now()}` })
|
||||||
|
await featurePage.createFeature(data)
|
||||||
|
|
||||||
|
let exists = await featurePage.featureExists(data.name)
|
||||||
|
expect(exists).toBe(true)
|
||||||
|
|
||||||
|
await featurePage.deleteFeature(data.name)
|
||||||
|
// No need to track for cleanup since we just deleted it
|
||||||
|
|
||||||
|
exists = await featurePage.featureExists(data.name)
|
||||||
|
expect(exists).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3d. Register the Page Object in Fixtures
|
||||||
|
|
||||||
|
If creating a brand new feature, add it to `core/fixtures/base.fixture.ts`:
|
||||||
|
|
||||||
|
1. Import the page class
|
||||||
|
2. Add to `BifrostFixtures` type
|
||||||
|
3. Add fixture definition
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In base.fixture.ts
|
||||||
|
import { FeaturePage } from '../../features/<feature>/pages/<feature>.page'
|
||||||
|
|
||||||
|
type BifrostFixtures = {
|
||||||
|
// ... existing
|
||||||
|
featurePage: FeaturePage
|
||||||
|
}
|
||||||
|
|
||||||
|
export const test = base.extend<BifrostFixtures>({
|
||||||
|
// ... existing
|
||||||
|
featurePage: async ({ page }, use) => {
|
||||||
|
await use(new FeaturePage(page))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3e. Add npm Script (optional)
|
||||||
|
|
||||||
|
In `tests/e2e/package.json`, add a feature-specific test script:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"test:<feature>": "playwright test features/<feature>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Run Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From tests/e2e/ directory
|
||||||
|
cd tests/e2e
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
npx playwright test
|
||||||
|
|
||||||
|
# Run specific feature
|
||||||
|
npx playwright test features/<feature>
|
||||||
|
|
||||||
|
# Run in headed mode (see the browser)
|
||||||
|
npx playwright test features/<feature> --headed
|
||||||
|
|
||||||
|
# Run with Playwright UI (interactive)
|
||||||
|
npx playwright test --ui
|
||||||
|
|
||||||
|
# Run with debug inspector
|
||||||
|
npx playwright test features/<feature> --debug
|
||||||
|
|
||||||
|
# Run a single test by title
|
||||||
|
npx playwright test -g "should create a basic item"
|
||||||
|
|
||||||
|
# From project root via Makefile
|
||||||
|
make run-e2e FLOW=<feature>
|
||||||
|
make run-e2e-headed FLOW=<feature>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Environment variables:**
|
||||||
|
- `BASE_URL` - Override app URL (default: http://localhost:3000)
|
||||||
|
- `BIFROST_BASE_URL` - Override Bifrost API URL (default: http://localhost:8080)
|
||||||
|
- `SKIP_WEB_SERVER=1` - Skip auto-starting Vite dev server
|
||||||
|
- `CI=1` - Enable CI mode (retries, serial execution)
|
||||||
|
|
||||||
|
## Step 5: Debug Failing Tests
|
||||||
|
|
||||||
|
### Common Issues and Fixes
|
||||||
|
|
||||||
|
**1. Element not found / timeout:**
|
||||||
|
- Check if the `data-testid` attribute exists in the UI component
|
||||||
|
- Verify the page has loaded: add `await waitForNetworkIdle(page)` after navigation
|
||||||
|
- Check for loading spinners: `await page.locator('[data-testid="loading-spinner"]').waitFor({ state: 'hidden' })`
|
||||||
|
|
||||||
|
**2. Toast interfering with clicks:**
|
||||||
|
- Dismiss toasts before interactions: `await page.dismissToasts()` or `await page.forceCloseToasts()`
|
||||||
|
|
||||||
|
**3. Sheet/dialog animation not complete:**
|
||||||
|
- Wait for animation: `await page.waitForSheetAnimation()`
|
||||||
|
- Wait for sheet visibility: `await expect(sheet).toBeVisible()`
|
||||||
|
|
||||||
|
**4. Stale element after table refresh:**
|
||||||
|
- Re-query the locator after data changes (don't reuse old locator references)
|
||||||
|
- Use polling patterns like `waitForVirtualKeyGone()` for eventual consistency
|
||||||
|
|
||||||
|
**5. Tests pass individually but fail together:**
|
||||||
|
- Check cleanup: ensure `afterEach` deletes all created resources
|
||||||
|
- Use unique names with `Date.now()` timestamps
|
||||||
|
- Check for state pollution between test files
|
||||||
|
|
||||||
|
**6. Flaky Radix/Shadcn Select:**
|
||||||
|
- Use the `fillSelect()` helper from `core/utils/test-helpers.ts`
|
||||||
|
- Wait for `[role="listbox"]` to appear before clicking options
|
||||||
|
- Wait for `[role="listbox"]` to disappear after selection
|
||||||
|
|
||||||
|
### Debugging Tools
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View the HTML report of the last run
|
||||||
|
npx playwright show-report
|
||||||
|
|
||||||
|
# Generate test code by recording browser actions
|
||||||
|
npx playwright codegen http://localhost:3000
|
||||||
|
|
||||||
|
# Run with trace viewer (records every action)
|
||||||
|
npx playwright test --trace on
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trace and Screenshots
|
||||||
|
|
||||||
|
- **Screenshots:** Taken automatically on failure, saved to `test-results/`
|
||||||
|
- **Traces:** Captured on first retry, viewable via `npx playwright show-trace <trace.zip>`
|
||||||
|
- **Videos:** Retained on failure, saved to `test-results/`
|
||||||
|
|
||||||
|
## Mandatory Rules
|
||||||
|
|
||||||
|
These rules MUST be followed at all times:
|
||||||
|
|
||||||
|
### Selectors
|
||||||
|
- **ALWAYS** use `data-testid` attributes as the primary selector strategy
|
||||||
|
- **ALWAYS** use `page.getByTestId()` or `page.getByRole()` - never raw CSS selectors for interactive elements
|
||||||
|
- **NEVER** use chained parent locators (`.locator('..')`)
|
||||||
|
- **NEVER** use `{ force: true }` on clicks - fix the underlying visibility issue instead
|
||||||
|
- If a needed `data-testid` doesn't exist in the UI, add it to the UI component first
|
||||||
|
|
||||||
|
### Waits
|
||||||
|
- **ALWAYS** use semantic waits (`waitFor()`, `expect().toBeVisible()`, `waitForLoadState()`)
|
||||||
|
- **NEVER** use `page.waitForTimeout()` except as last resort in cleanup/polling (and document why)
|
||||||
|
- **ALWAYS** wait for page load after navigation: `await waitForNetworkIdle(page)`
|
||||||
|
- **ALWAYS** wait for sheet animations before interacting with sheet contents
|
||||||
|
|
||||||
|
### Test Data
|
||||||
|
- **ALWAYS** use `Date.now()` or `TestDataFactory.uniqueId()` for unique test names
|
||||||
|
- **NEVER** use static/hardcoded test data names (causes collisions in parallel runs)
|
||||||
|
- **ALWAYS** create test data factory functions in `<feature>.data.ts`
|
||||||
|
|
||||||
|
### Cleanup
|
||||||
|
- **ALWAYS** track created resources in arrays and delete them in `afterEach`
|
||||||
|
- **ALWAYS** close open sheets before cleanup: `await page.closeSheet()`
|
||||||
|
- **ALWAYS** dismiss toasts before interactive operations
|
||||||
|
- Cleanup failures should `console.error` and continue, never throw
|
||||||
|
|
||||||
|
### Assertions
|
||||||
|
- **ALWAYS** write deterministic assertions that can actually fail
|
||||||
|
- **NEVER** write `expect(count >= 0).toBe(true)` - this always passes
|
||||||
|
- Use count-based branching for state-dependent assertions:
|
||||||
|
```typescript
|
||||||
|
if (count === 0) {
|
||||||
|
await expect(emptyState).toBeVisible()
|
||||||
|
} else {
|
||||||
|
expect(count).toBeGreaterThan(0)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
- **ALWAYS** import `test` and `expect` from `../../core/fixtures/base.fixture`
|
||||||
|
- **NEVER** import directly from `@playwright/test` in spec files (use the custom fixture)
|
||||||
|
|
||||||
|
### Adding data-testid to UI
|
||||||
|
|
||||||
|
When a UI component is missing a required `data-testid`, add it directly. Convention:
|
||||||
|
```
|
||||||
|
data-testid="<entity>-<element>-<qualifier>"
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
data-testid="vk-row-{name}" # Virtual key table row
|
||||||
|
data-testid="vk-edit-btn-{name}" # Edit button for specific VK
|
||||||
|
data-testid="vk-delete-btn-{name}" # Delete button for specific VK
|
||||||
|
data-testid="create-vk-btn" # Create button
|
||||||
|
data-testid="vk-sheet" # Virtual key form sheet
|
||||||
|
data-testid="vk-name-input" # Name input in VK form
|
||||||
|
data-testid="vk-save-btn" # Save button in VK form
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available BasePage Methods
|
||||||
|
|
||||||
|
Every page object inherits these from `BasePage` (`core/pages/base.page.ts`):
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `waitForPageLoad()` | Wait for `networkidle` load state |
|
||||||
|
| `waitForChartsToLoad()` | Wait for charts/data and skeletons to disappear |
|
||||||
|
| `getToast(type?)` | Get toast locator (success/error/loading/default) |
|
||||||
|
| `waitForSuccessToast(message?)` | Wait for success toast, optionally match message |
|
||||||
|
| `waitForErrorToast(message?)` | Wait for error toast, optionally match message |
|
||||||
|
| `waitForToastsToDisappear(timeout?)` | Wait for all toasts to be gone |
|
||||||
|
| `dismissToasts()` | Wait for toasts to auto-dismiss |
|
||||||
|
| `forceCloseToasts()` | Click away + wait to force-dismiss toasts |
|
||||||
|
| `waitForSheetAnimation()` | Wait for sheet/dialog open animation to complete |
|
||||||
|
| `waitForStateChange(locator, attr, val)` | Wait for element attribute to match |
|
||||||
|
| `fillByLabel(label, value)` | Fill input by its label |
|
||||||
|
| `fillByPlaceholder(placeholder, value)` | Fill input by its placeholder |
|
||||||
|
| `fillByTestId(testId, value)` | Fill input by data-testid |
|
||||||
|
| `clickButton(text)` | Click button by visible text |
|
||||||
|
| `clickByTestId(testId)` | Click element by data-testid |
|
||||||
|
| `closeDevProfiler()` | Dismiss the Dev Profiler overlay if visible |
|
||||||
|
|
||||||
|
## Available Test Helpers (`core/utils/test-helpers.ts`)
|
||||||
|
|
||||||
|
| Helper | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `waitForNetworkIdle(page, timeout?)` | Wait for network idle state |
|
||||||
|
| `wait(ms)` | Sleep for ms (use sparingly) |
|
||||||
|
| `retry(fn, { retries, delay })` | Retry async function with backoff |
|
||||||
|
| `randomString(length?)` | Generate random alphanumeric string |
|
||||||
|
| `uniqueTestName(prefix)` | Generate unique name with timestamp + random suffix |
|
||||||
|
| `assertToast(page, text, type)` | Assert toast appears with text |
|
||||||
|
| `assertUrl(page, pattern)` | Assert page URL matches pattern |
|
||||||
|
| `fillSelect(page, triggerSelector, optionText)` | Fill Radix/Shadcn Select component |
|
||||||
|
| `fillMultiSelect(page, inputSelector, values)` | Fill multi-select with array of values |
|
||||||
|
| `clearAndFill(page, selector, value)` | Atomically clear and fill input |
|
||||||
|
| `getTableRowCount(page, tableSelector)` | Get count of table body rows |
|
||||||
|
| `tableContainsRow(page, tableSelector, text)` | Check if table has row with text |
|
||||||
|
| `waitForTableLoad(page, tableSelector)` | Wait for table visible + spinner gone |
|
||||||
|
| `screenshotOnError(page, testName, fn)` | Auto-screenshot wrapper for debugging |
|
||||||
|
|
||||||
|
## Available API Helpers (`core/actions/api.ts`)
|
||||||
|
|
||||||
|
For programmatic setup/cleanup via API (bypassing UI):
|
||||||
|
|
||||||
|
| API | Methods |
|
||||||
|
|-----|---------|
|
||||||
|
| `providersApi` | `getAll`, `get`, `create`, `update`, `delete` |
|
||||||
|
| `virtualKeysApi` | `getAll`, `get`, `create`, `update`, `delete` |
|
||||||
|
| `teamsApi` | `getAll`, `create`, `delete` |
|
||||||
|
| `customersApi` | `getAll`, `create`, `delete` |
|
||||||
|
| `cleanupTestData(request, { virtualKeyIds, teamIds, customerIds, providerNames })` | Bulk cleanup |
|
||||||
1
.claude/skills/expect
Symbolic link
1
.claude/skills/expect
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../.agents/skills/expect
|
||||||
648
.claude/skills/investigate-issue/SKILL.md
Normal file
648
.claude/skills/investigate-issue/SKILL.md
Normal file
@@ -0,0 +1,648 @@
|
|||||||
|
---
|
||||||
|
name: investigate-issue
|
||||||
|
description: Investigate a GitHub issue by fetching details, analyzing the codebase, researching documentation, and presenting an actionable implementation plan with test guidance. Use when asked to investigate, analyze, triage, or plan work for a GitHub issue. Invoked with /investigate-issue <ISSUE_ID> or /investigate-issue (prompts for ID).
|
||||||
|
allowed-tools: Read, Grep, Glob, Bash, WebSearch, WebFetch, mcp__context7__resolve-library-id, mcp__context7__query-docs, Task, AskUserQuestion, TodoWrite, Edit, Write
|
||||||
|
---
|
||||||
|
|
||||||
|
# Investigate GitHub Issue
|
||||||
|
|
||||||
|
Fetch a GitHub issue, analyze the report, search the codebase for relevant code, research external documentation, and present a comprehensive implementation plan with side-effect analysis and test guidance.
|
||||||
|
|
||||||
|
**Your final report MUST contain all of these sections:**
|
||||||
|
1. Issue Details (from Step 1)
|
||||||
|
2. Classification (from Step 2)
|
||||||
|
3. Codebase Analysis + Documentation Research (from Step 3, including sub-step 3e)
|
||||||
|
4. Impact Analysis (from Step 4)
|
||||||
|
5. Test Plan (from Step 5)
|
||||||
|
6. Full Presentation (Step 6 template)
|
||||||
|
|
||||||
|
If any section is missing, go back and complete it before presenting the report.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/investigate-issue <ISSUE_ID> # Investigate issue by number
|
||||||
|
/investigate-issue # Prompts for issue ID
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow Overview
|
||||||
|
|
||||||
|
1. **Get the issue** -- Fetch full issue details from GitHub
|
||||||
|
2. **Classify the issue** -- Determine type (bug, feature, docs) and affected areas
|
||||||
|
3. **Search the codebase and research docs** -- Find relevant code, then research the libraries it depends on via Context7 and WebSearch
|
||||||
|
4. **Analyze impact** -- Cross-reference codebase findings with documentation to identify side effects, dependencies, and breaking changes
|
||||||
|
5. **Suggest tests** -- If changes touch `core/`, recommend specific LLM and MCP test additions
|
||||||
|
6. **Present the plan** -- Show findings and recommended changes to the user
|
||||||
|
7. **Implement with approval** -- After plan approval, make changes one at a time with user confirmation
|
||||||
|
|
||||||
|
## Step 1: Fetch the Issue
|
||||||
|
|
||||||
|
If no issue ID is provided, ask the user:
|
||||||
|
```
|
||||||
|
What is the GitHub issue number you want to investigate?
|
||||||
|
```
|
||||||
|
|
||||||
|
The repository is always `maximhq/bifrost`.
|
||||||
|
|
||||||
|
Fetch full issue details:
|
||||||
|
```bash
|
||||||
|
# Get issue with all metadata
|
||||||
|
gh issue view <ISSUE_ID> --repo maximhq/bifrost --json number,title,body,labels,assignees,state,comments,author,createdAt,updatedAt
|
||||||
|
|
||||||
|
# Get issue comments for additional context
|
||||||
|
gh issue view <ISSUE_ID> --repo maximhq/bifrost --json comments --jq '.comments[].body'
|
||||||
|
```
|
||||||
|
|
||||||
|
If the issue does not exist or `gh` fails:
|
||||||
|
- Check authentication: `gh auth status`
|
||||||
|
- Verify the issue number is valid: `gh issue list --repo maximhq/bifrost --limit 5 --json number,title`
|
||||||
|
- Report the error and ask the user for a corrected issue ID
|
||||||
|
|
||||||
|
## Step 2: Classify the Issue
|
||||||
|
|
||||||
|
### 2a. Determine Issue Type
|
||||||
|
|
||||||
|
Parse the issue title prefix and labels to classify:
|
||||||
|
|
||||||
|
| Title Prefix | Label | Type | Investigation Focus |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `[Bug]:` | `bug` | Bug | Reproduce, find root cause, identify fix |
|
||||||
|
| `[Feature]:` | `enhancement` | Feature | Design approach, find insertion points |
|
||||||
|
| `[Docs]:` | `documentation` | Docs | Find affected doc pages, verify accuracy |
|
||||||
|
| (none) | (any) | General | Read body carefully, infer type from content |
|
||||||
|
|
||||||
|
### 2b. Determine Affected Areas
|
||||||
|
|
||||||
|
Use the issue's labels and body content to map to codebase areas. The issue templates include an "Affected area(s)" field with these values:
|
||||||
|
|
||||||
|
| Area Label | Codebase Directories | Key Files |
|
||||||
|
|---|---|---|
|
||||||
|
| Core (Go) | `core/`, `core/schemas/`, `core/providers/`, `core/mcp/` | `core/bifrost.go`, `core/utils.go` |
|
||||||
|
| Framework | `framework/`, `framework/configstore/`, `framework/logstore/` | `framework/config.go`, `framework/list.go` |
|
||||||
|
| Transports (HTTP) | `transports/bifrost-http/` | `transports/bifrost-http/` |
|
||||||
|
| Plugins | `plugins/` (governance, jsonparser, litellmcompat, etc.) | Plugin-specific `go.mod` files |
|
||||||
|
| UI (React) | `ui/`, `ui/app/workspace/`, `ui/components/` | Feature-specific workspace pages |
|
||||||
|
| Docs | `docs/` | `docs/docs.json`, feature-specific `.mdx` files |
|
||||||
|
|
||||||
|
If the issue body mentions specific providers (e.g., "openai", "anthropic", "gemini"), also search:
|
||||||
|
```bash
|
||||||
|
ls core/providers/
|
||||||
|
# Maps to: core/providers/<provider_name>/
|
||||||
|
```
|
||||||
|
|
||||||
|
If the issue mentions MCP, agents, or tools, also search:
|
||||||
|
```bash
|
||||||
|
ls core/mcp/
|
||||||
|
# Key: core/mcp/agent.go, core/mcp/codemode.go, core/mcp/toolmanager.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2c. Extract Key Information
|
||||||
|
|
||||||
|
From the issue body, extract and summarize:
|
||||||
|
- **What is reported** -- The specific problem or request
|
||||||
|
- **Reproduction steps** -- If bug, how to trigger it
|
||||||
|
- **Expected vs actual behavior** -- What should happen vs what happens
|
||||||
|
- **Version info** -- Which version is affected
|
||||||
|
- **Environment details** -- OS, Go version, Node version, etc.
|
||||||
|
- **Code snippets** -- Any code the reporter included
|
||||||
|
- **Error messages** -- Stack traces, logs, error output
|
||||||
|
|
||||||
|
## Step 3: Search the Codebase
|
||||||
|
|
||||||
|
Systematically search for all code relevant to the issue. Use multiple search strategies:
|
||||||
|
|
||||||
|
### 3a. Keyword Search
|
||||||
|
|
||||||
|
Extract key terms from the issue and search:
|
||||||
|
```bash
|
||||||
|
# Search for error messages mentioned in the issue
|
||||||
|
grep -rn "exact error message" core/ framework/ transports/ ui/
|
||||||
|
|
||||||
|
# Search for function/type names mentioned
|
||||||
|
grep -rn "FunctionName\|TypeName" --include='*.go' core/
|
||||||
|
grep -rn "componentName\|functionName" --include='*.ts' --include='*.tsx' ui/
|
||||||
|
|
||||||
|
# Search for API endpoints mentioned
|
||||||
|
grep -rn "/api/endpoint" transports/ ui/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3b. Structural Search by Area
|
||||||
|
|
||||||
|
Based on the affected area, do targeted exploration:
|
||||||
|
|
||||||
|
**For Core (Go) issues:**
|
||||||
|
```bash
|
||||||
|
# Find the specific provider if mentioned
|
||||||
|
ls core/providers/<provider>/
|
||||||
|
grep -rn "relevantFunction" core/providers/<provider>/
|
||||||
|
|
||||||
|
# Check schemas for relevant types
|
||||||
|
grep -rn "TypeName" core/schemas/ --include='*.go'
|
||||||
|
|
||||||
|
# Check the main bifrost.go for relevant handlers
|
||||||
|
grep -n "functionName\|handlerName" core/bifrost.go
|
||||||
|
```
|
||||||
|
|
||||||
|
**For MCP/Agent issues:**
|
||||||
|
```bash
|
||||||
|
# Search agent code
|
||||||
|
grep -rn "keyword" core/mcp/ --include='*.go'
|
||||||
|
|
||||||
|
# Check codemode if relevant
|
||||||
|
ls core/mcp/codemode/
|
||||||
|
grep -rn "keyword" core/mcp/codemode/ --include='*.go'
|
||||||
|
```
|
||||||
|
|
||||||
|
**For UI issues:**
|
||||||
|
```bash
|
||||||
|
# Find the workspace page
|
||||||
|
ls ui/app/workspace/<feature>/
|
||||||
|
|
||||||
|
# Search for components
|
||||||
|
grep -rn "keyword" ui/app/workspace/<feature>/ --include='*.tsx' --include='*.ts'
|
||||||
|
grep -rn "keyword" ui/components/ --include='*.tsx' --include='*.ts'
|
||||||
|
|
||||||
|
# Check for data-testid attributes (relevant for E2E impact)
|
||||||
|
grep -rn 'data-testid' ui/app/workspace/<feature>/ --include='*.tsx'
|
||||||
|
```
|
||||||
|
|
||||||
|
**For Framework issues:**
|
||||||
|
```bash
|
||||||
|
grep -rn "keyword" framework/ --include='*.go'
|
||||||
|
ls framework/configstore/ framework/logstore/ framework/plugins/
|
||||||
|
```
|
||||||
|
|
||||||
|
**For Plugin issues:**
|
||||||
|
```bash
|
||||||
|
# Identify which plugin
|
||||||
|
ls plugins/
|
||||||
|
grep -rn "keyword" plugins/<plugin_name>/ --include='*.go'
|
||||||
|
```
|
||||||
|
|
||||||
|
**For Docs issues:**
|
||||||
|
```bash
|
||||||
|
# Find the affected documentation page
|
||||||
|
find docs/ -name "*.mdx" | head -30
|
||||||
|
grep -rn "keyword" docs/ --include='*.mdx'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3c. Dependency Tracing
|
||||||
|
|
||||||
|
For any function or type identified as needing changes, trace its callers and dependents:
|
||||||
|
```bash
|
||||||
|
# Find all callers of a function
|
||||||
|
grep -rn "FunctionName(" --include='*.go' core/ framework/ transports/ plugins/
|
||||||
|
|
||||||
|
# Find all implementations of an interface
|
||||||
|
grep -rn "InterfaceName" --include='*.go' core/ framework/
|
||||||
|
|
||||||
|
# Find all imports of a package
|
||||||
|
grep -rn '"github.com/maximhq/bifrost/core/schemas"' --include='*.go' .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3d. Find Related Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find existing tests for the affected code
|
||||||
|
grep -rn "TestFunctionName\|Test.*Relevant" --include='*_test.go' core/ framework/ transports/
|
||||||
|
|
||||||
|
# Check LLM tests
|
||||||
|
grep -rn "keyword" core/internal/llmtests/ --include='*.go'
|
||||||
|
|
||||||
|
# Check MCP tests
|
||||||
|
grep -rn "keyword" core/internal/mcptests/ --include='*_test.go'
|
||||||
|
|
||||||
|
# Check E2E tests if UI is affected
|
||||||
|
grep -rn "keyword" tests/e2e/ --include='*.ts'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3e. Research External Documentation
|
||||||
|
|
||||||
|
Now that you've found the relevant code, research the external libraries it depends on. This informs the impact analysis in Step 4.
|
||||||
|
|
||||||
|
**Identify libraries from the code you found:**
|
||||||
|
```bash
|
||||||
|
# Check go.mod for dependencies relevant to the issue
|
||||||
|
cat core/go.mod | grep -v "^$" | grep -v "//"
|
||||||
|
|
||||||
|
# For UI issues, check package.json
|
||||||
|
cat ui/package.json | jq '.dependencies, .devDependencies'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query Context7 for each relevant library:**
|
||||||
|
```
|
||||||
|
# Step 1: Resolve the library ID
|
||||||
|
mcp__context7__resolve-library-id(
|
||||||
|
libraryName: "<library name from go.mod or package.json>",
|
||||||
|
query: "<issue title + key terms>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 2: Query the docs with the resolved ID
|
||||||
|
mcp__context7__query-docs(
|
||||||
|
libraryId: "<result from step 1>",
|
||||||
|
query: "<specific question about behavior relevant to the issue>"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Common libraries: `mark3labs/mcp-go` (MCP protocol), `stretchr/testify` (test assertions), `react` (UI framework), `playwright` (E2E testing), provider SDKs (OpenAI, Anthropic, etc.)
|
||||||
|
|
||||||
|
**Search the web for additional context:**
|
||||||
|
```
|
||||||
|
WebSearch: "<error message or behavior from the issue> <library name>"
|
||||||
|
WebSearch: "<library name> best practices <relevant pattern>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Record your findings in this table** (you will copy it into the final report in Step 6):
|
||||||
|
|
||||||
|
| Source | Query Used | Key Finding | Link |
|
||||||
|
|--------|-----------|-------------|------|
|
||||||
|
| Context7: `<library>` | `<query>` | `<what was learned, or "No relevant docs found">` | `<link if available>` |
|
||||||
|
| WebSearch | `<query>` | `<what was learned, or "No relevant results">` | `<URL>` |
|
||||||
|
|
||||||
|
If no useful results are found for a library, still include a row with "No relevant documentation found" and note what was searched. This transparency helps the user understand research coverage.
|
||||||
|
|
||||||
|
## Step 4: Analyze Impact
|
||||||
|
|
||||||
|
Using both the codebase search results (Step 3a-3d) and the documentation research (Step 3e), analyze the impact of the proposed changes. For each file, note whether library documentation revealed any constraints or best practices that affect the implementation.
|
||||||
|
|
||||||
|
### 4a. Direct Changes Required
|
||||||
|
|
||||||
|
For each file that needs modification, document:
|
||||||
|
- **File path** (absolute)
|
||||||
|
- **What to change** (function, type, handler, component)
|
||||||
|
- **Why** (ties back to the issue)
|
||||||
|
- **How** (specific approach -- add parameter, modify logic, new function, etc.)
|
||||||
|
- **Library constraints** (from Step 3e research -- any API contracts, deprecations, or version requirements)
|
||||||
|
|
||||||
|
### 4b. Side Effects Analysis
|
||||||
|
|
||||||
|
For each proposed change, trace the blast radius:
|
||||||
|
|
||||||
|
**Code side effects:**
|
||||||
|
```bash
|
||||||
|
# Who calls this function?
|
||||||
|
grep -rn "FunctionToChange(" --include='*.go' core/ framework/ transports/ plugins/
|
||||||
|
|
||||||
|
# Who uses this type?
|
||||||
|
grep -rn "TypeToChange" --include='*.go' core/ framework/ transports/ plugins/
|
||||||
|
|
||||||
|
# What tests exercise this code?
|
||||||
|
grep -rn "FunctionToChange\|TestRelated" --include='*_test.go' core/ framework/ transports/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Schema side effects (if changing types in core/schemas/):**
|
||||||
|
- Check all providers that implement the schema
|
||||||
|
- Check framework code that serializes/deserializes the type
|
||||||
|
- Check UI code that consumes the API response
|
||||||
|
- Check plugin code that uses the schema
|
||||||
|
|
||||||
|
**API side effects (if changing endpoints):**
|
||||||
|
- Check all UI pages that call the endpoint
|
||||||
|
- Check E2E tests that hit the endpoint
|
||||||
|
- Check documentation that references the endpoint
|
||||||
|
|
||||||
|
**UI side effects (if changing components):**
|
||||||
|
- Check all pages that use the component
|
||||||
|
- Check E2E tests with selectors targeting the component
|
||||||
|
- Check if data-testid attributes change
|
||||||
|
|
||||||
|
### 4c. Breaking Change Assessment
|
||||||
|
|
||||||
|
Classify the change:
|
||||||
|
- **Non-breaking** -- Internal refactor, bug fix with same API surface
|
||||||
|
- **Minor breaking** -- New required parameter with default, deprecation
|
||||||
|
- **Major breaking** -- Changed API signature, removed field, behavioral change
|
||||||
|
|
||||||
|
## Step 5: Test Recommendations
|
||||||
|
|
||||||
|
### 5a. General Test Guidance
|
||||||
|
|
||||||
|
For ANY code change, identify:
|
||||||
|
- Existing tests that need updating
|
||||||
|
- New test cases that should be added
|
||||||
|
- Edge cases to cover
|
||||||
|
|
||||||
|
### 5b. LLM Tests (when changes touch `core/` or `core/providers/`)
|
||||||
|
|
||||||
|
The LLM test infrastructure lives in `core/internal/llmtests/`. Key patterns:
|
||||||
|
|
||||||
|
**Test structure:**
|
||||||
|
- Each scenario is a self-contained Go file with a `Run{Scenario}Test()` function
|
||||||
|
- Signature: `func Run{Scenario}Test(t *testing.T, client *bifrost.Bifrost, ctx context.Context, testConfig ComprehensiveTestConfig)`
|
||||||
|
- Tests run against both Chat Completions and Responses APIs (dual-API testing)
|
||||||
|
|
||||||
|
**How to add a new LLM test:**
|
||||||
|
1. Create a new file in `core/internal/llmtests/` named after the scenario (e.g., `new_scenario.go`)
|
||||||
|
2. Implement `RunNewScenarioTest()` following the signature pattern
|
||||||
|
3. Register it in `core/internal/llmtests/tests.go` by adding to the `testScenarios` slice
|
||||||
|
4. Add a `Scenarios` flag in `ComprehensiveTestConfig` to enable/disable it
|
||||||
|
5. Use `validation_presets.go` expectations (e.g., `BasicChatExpectations()`, `ToolCallExpectations()`)
|
||||||
|
6. Use the retry framework from `test_retry_framework.go` for validation failures
|
||||||
|
|
||||||
|
**Example test recommendation format:**
|
||||||
|
```
|
||||||
|
New LLM test: Run{X}Test in core/internal/llmtests/{x}.go
|
||||||
|
- Scenario: <what it tests>
|
||||||
|
- Expectations: <which validation preset to use>
|
||||||
|
- Dual-API: Yes, test both Chat Completions and Responses API paths
|
||||||
|
- Register in: tests.go testScenarios slice
|
||||||
|
- Config flag: Scenarios.{X} in ComprehensiveTestConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
**Running LLM tests:**
|
||||||
|
```bash
|
||||||
|
make test-core PROVIDER=<provider> TESTCASE=<TestName>
|
||||||
|
make test-core PROVIDER=<provider> PATTERN=<substring>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5c. MCP Tests (when changes touch `core/mcp/`)
|
||||||
|
|
||||||
|
The MCP test infrastructure lives in `core/internal/mcptests/`. Key patterns:
|
||||||
|
|
||||||
|
**Test structure:**
|
||||||
|
- Standard Go test functions: `func TestScenario(t *testing.T)`
|
||||||
|
- Setup via `SetupAgentTest(t, AgentTestConfig{...})` which returns `(manager, mocker, ctx)`
|
||||||
|
- Mock LLM responses via `DynamicLLMMocker` with a response queue
|
||||||
|
- Agent tests use `MockLLMCaller` with pre-defined response sequences
|
||||||
|
|
||||||
|
**How to add a new MCP test:**
|
||||||
|
1. Identify which test file category it belongs to:
|
||||||
|
- `agent_*_test.go` -- Agent loop behavior tests
|
||||||
|
- `tool_*_test.go` -- Tool execution tests
|
||||||
|
- `connection_*_test.go` -- Client connection tests
|
||||||
|
- `codemode_*_test.go` -- CodeMode-specific tests
|
||||||
|
2. Create the test function following existing patterns
|
||||||
|
3. Use `AgentTestConfig` for declarative setup:
|
||||||
|
```go
|
||||||
|
manager, mocker, ctx := SetupAgentTest(t, AgentTestConfig{
|
||||||
|
InProcessTools: []string{"echo", "calculator"},
|
||||||
|
AutoExecuteTools: []string{"*"},
|
||||||
|
MaxDepth: 5,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
4. Use assertion helpers: `AssertAgentCompletedInTurns()`, `AssertToolExecutedInTurn()`
|
||||||
|
5. Use fixture helpers from `fixtures.go` for mock responses
|
||||||
|
|
||||||
|
**Example test recommendation format:**
|
||||||
|
```
|
||||||
|
New MCP test: Test{X} in core/internal/mcptests/{category}_test.go
|
||||||
|
- Category: agent | tool | connection | codemode
|
||||||
|
- Setup: AgentTestConfig with {tools} and {config}
|
||||||
|
- Mock responses: {describe the LLM response sequence}
|
||||||
|
- Assertions: {what to verify}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Running MCP tests:**
|
||||||
|
```bash
|
||||||
|
make test-mcp TESTCASE=<TestName>
|
||||||
|
make test-mcp TYPE=<category> PATTERN=<substring>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5d. E2E Tests (when changes touch `ui/`)
|
||||||
|
|
||||||
|
If UI changes are involved, recommend E2E test updates following the patterns in `tests/e2e/`:
|
||||||
|
- Page objects in `tests/e2e/features/<feature>/pages/`
|
||||||
|
- Specs in `tests/e2e/features/<feature>/`
|
||||||
|
- Reference the `/e2e-test` skill for full E2E test creation workflow
|
||||||
|
- **Never marshal API payloads to a `Record`/`Map`** — construct payloads as object literals with fields in the intended order and pass directly to Playwright's `request.post({ data })`. Marshaling through intermediate maps can reorder fields, breaking backend validation and snapshot comparisons.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make run-e2e FLOW=<feature>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 6: Present Findings
|
||||||
|
|
||||||
|
Present everything to the user in this structured format:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Issue Investigation: #<ID> -- <Title>
|
||||||
|
|
||||||
|
### Issue Classification
|
||||||
|
- **Type:** Bug / Feature / Docs
|
||||||
|
- **Severity:** <from issue if present>
|
||||||
|
- **Affected areas:** <list>
|
||||||
|
- **Labels:** <list>
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
<2-3 sentence summary of what the issue is about and what needs to happen>
|
||||||
|
|
||||||
|
### Codebase Analysis
|
||||||
|
|
||||||
|
#### Relevant Files Found
|
||||||
|
| File | Relevance | What Needs to Change |
|
||||||
|
|------|-----------|---------------------|
|
||||||
|
| `<absolute path>` | <why this file matters> | <specific change needed> |
|
||||||
|
| ... | ... | ... |
|
||||||
|
|
||||||
|
#### Current Behavior
|
||||||
|
<Describe what the code currently does, with code snippets>
|
||||||
|
|
||||||
|
#### Root Cause (for bugs) / Design Gap (for features)
|
||||||
|
<Explain why the issue exists>
|
||||||
|
|
||||||
|
### Documentation Research
|
||||||
|
|
||||||
|
Copy the research table from Step 3e here. If you skipped Step 3e, go back and do it now before presenting.
|
||||||
|
|
||||||
|
#### Libraries & References Consulted
|
||||||
|
| Source | Query Used | Key Finding | Link |
|
||||||
|
|--------|-----------|-------------|------|
|
||||||
|
| <from Step 3e table> | <query> | <finding> | <link> |
|
||||||
|
|
||||||
|
#### How Documentation Informs the Plan
|
||||||
|
<For each change above, note any library constraints, best practices, or API contracts discovered in Step 3e that shaped the approach. Reference specific Change numbers.>
|
||||||
|
|
||||||
|
### Implementation Plan
|
||||||
|
|
||||||
|
#### Changes Required (in order)
|
||||||
|
|
||||||
|
**Change 1: <File path>**
|
||||||
|
- **What:** <specific modification>
|
||||||
|
- **Why:** <ties to issue>
|
||||||
|
- **Function/Component:** `<name>`
|
||||||
|
- **Approach:** <how to implement>
|
||||||
|
|
||||||
|
**Change 2: <File path>**
|
||||||
|
- ...
|
||||||
|
|
||||||
|
#### Side Effects
|
||||||
|
| Change | Affected Code | Risk | Mitigation |
|
||||||
|
|--------|--------------|------|------------|
|
||||||
|
| Change 1 | `<caller/dependent>` | <risk level> | <how to mitigate> |
|
||||||
|
|
||||||
|
#### Breaking Changes
|
||||||
|
- <list any breaking changes, or "None expected">
|
||||||
|
|
||||||
|
### Test Plan
|
||||||
|
|
||||||
|
#### Existing Tests to Update
|
||||||
|
| Test | File | What to Change |
|
||||||
|
|------|------|---------------|
|
||||||
|
| `TestName` | `<path>` | <modification needed> |
|
||||||
|
|
||||||
|
#### New Tests to Add
|
||||||
|
| Test | File | What It Covers |
|
||||||
|
|------|------|---------------|
|
||||||
|
| `TestNewScenario` | `<path>` | <scenario description> |
|
||||||
|
|
||||||
|
<If changes touch core/>
|
||||||
|
#### LLM Test Additions
|
||||||
|
<Specific LLM test recommendations per Section 5b format>
|
||||||
|
|
||||||
|
#### MCP Test Additions
|
||||||
|
<Specific MCP test recommendations per Section 5c format>
|
||||||
|
</If>
|
||||||
|
|
||||||
|
<If changes touch ui/>
|
||||||
|
#### E2E Test Additions
|
||||||
|
<Recommend using /e2e-test skill for full test creation>
|
||||||
|
</If>
|
||||||
|
|
||||||
|
### Estimated Complexity
|
||||||
|
- **Scope:** Small (1-2 files) / Medium (3-5 files) / Large (6+ files)
|
||||||
|
- **Risk:** Low / Medium / High
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Proceed with implementation?** (yes / no / modify plan)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 7: Implement with Per-Change Approval
|
||||||
|
|
||||||
|
Once the user approves the plan:
|
||||||
|
|
||||||
|
### 7a. Create a Todo List
|
||||||
|
|
||||||
|
Create a todo item for each change in the plan:
|
||||||
|
```
|
||||||
|
1. Change 1: <description> -- pending
|
||||||
|
2. Change 2: <description> -- pending
|
||||||
|
3. Update test: <description> -- pending
|
||||||
|
4. Add new test: <description> -- pending
|
||||||
|
5. Verify all tests pass -- pending
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7b. For Each Change
|
||||||
|
|
||||||
|
Before making any edit, present the change to the user:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Change <N>/<Total>: <File path>
|
||||||
|
|
||||||
|
**What:** <description of the change>
|
||||||
|
|
||||||
|
**Current code:**
|
||||||
|
<existing code that will be modified>
|
||||||
|
|
||||||
|
**Proposed change:**
|
||||||
|
<new code after modification>
|
||||||
|
|
||||||
|
**Apply this change?** (yes / no / modify)
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait for user approval before applying. If user says "no", skip and move to the next change. If user says "modify", discuss and adjust.
|
||||||
|
|
||||||
|
### 7c. After All Changes
|
||||||
|
|
||||||
|
Once all approved changes are applied:
|
||||||
|
|
||||||
|
1. Run relevant tests:
|
||||||
|
```bash
|
||||||
|
# For core changes
|
||||||
|
make test-core PROVIDER=<relevant_provider> PATTERN=<relevant_test>
|
||||||
|
|
||||||
|
# For MCP changes
|
||||||
|
make test-mcp PATTERN=<relevant_test>
|
||||||
|
|
||||||
|
# For framework changes
|
||||||
|
cd framework && go test ./...
|
||||||
|
|
||||||
|
# For UI changes
|
||||||
|
make run-e2e FLOW=<feature>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Report results to the user
|
||||||
|
3. If tests fail, investigate and propose fixes (with approval)
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Issue Not Found
|
||||||
|
```
|
||||||
|
Issue #<ID> was not found in maximhq/bifrost.
|
||||||
|
- Verify the issue number is correct
|
||||||
|
- Run: gh issue list --repo maximhq/bifrost --limit 10 --json number,title
|
||||||
|
```
|
||||||
|
|
||||||
|
### gh CLI Not Authenticated
|
||||||
|
```
|
||||||
|
GitHub CLI is not authenticated. Run:
|
||||||
|
gh auth login
|
||||||
|
Then retry /investigate-issue <ID>
|
||||||
|
```
|
||||||
|
|
||||||
|
### No Relevant Code Found
|
||||||
|
If codebase search yields no results:
|
||||||
|
1. Broaden the search terms
|
||||||
|
2. Search for related concepts instead of exact matches
|
||||||
|
3. Ask the user for more context about where the code might live
|
||||||
|
4. Check if this is a net-new feature with no existing code
|
||||||
|
|
||||||
|
### External Documentation Not Found
|
||||||
|
If Context7 or WebSearch yield no useful results in Step 3e:
|
||||||
|
1. Still include a row in the research table with "No relevant documentation found" and note what was searched
|
||||||
|
2. Proceed with codebase analysis alone
|
||||||
|
3. Flag areas where documentation review might be needed before implementation
|
||||||
|
|
||||||
|
## Project Directory Reference
|
||||||
|
|
||||||
|
Quick reference for navigating the Bifrost codebase:
|
||||||
|
|
||||||
|
```
|
||||||
|
bifrost/
|
||||||
|
├── core/ # Go core library
|
||||||
|
│ ├── bifrost.go # Main Bifrost implementation (~195K)
|
||||||
|
│ ├── schemas/ # All Go types/schemas
|
||||||
|
│ ├── providers/ # Provider implementations (openai, anthropic, gemini, etc.)
|
||||||
|
│ ├── mcp/ # MCP protocol implementation
|
||||||
|
│ │ ├── agent.go # Agent mode
|
||||||
|
│ │ ├── codemode/ # CodeMode (Starlark-based)
|
||||||
|
│ │ └── toolmanager.go # Tool management
|
||||||
|
│ └── internal/
|
||||||
|
│ ├── llmtests/ # LLM integration tests (~48 files)
|
||||||
|
│ │ ├── setup.go # Test initialization
|
||||||
|
│ │ ├── tests.go # Test orchestrator (scenario registry)
|
||||||
|
│ │ ├── validation_presets.go # Reusable expectations
|
||||||
|
│ │ └── test_retry_framework.go # Retry logic
|
||||||
|
│ └── mcptests/ # MCP/Agent tests (~40 files)
|
||||||
|
│ ├── setup_test.go # Test infrastructure
|
||||||
|
│ ├── agent_test_helpers.go # AgentTestConfig + SetupAgentTest
|
||||||
|
│ └── fixtures.go # Mock servers & fixtures
|
||||||
|
├── framework/ # Framework layer
|
||||||
|
│ ├── configstore/ # Configuration storage
|
||||||
|
│ ├── logstore/ # Log storage
|
||||||
|
│ ├── plugins/ # Plugin system
|
||||||
|
│ └── streaming/ # Streaming utilities
|
||||||
|
├── transports/
|
||||||
|
│ └── bifrost-http/ # HTTP transport + Docker
|
||||||
|
├── ui/ # React + Vite UI
|
||||||
|
│ ├── app/workspace/ # Feature pages
|
||||||
|
│ └── components/ # Shared components
|
||||||
|
├── plugins/ # Go plugins (governance, otel, etc.)
|
||||||
|
├── docs/ # Mintlify documentation
|
||||||
|
├── tests/e2e/ # Playwright E2E tests
|
||||||
|
└── Makefile # Build & test commands
|
||||||
|
```
|
||||||
|
|
||||||
|
## Makefile Test Commands Reference
|
||||||
|
**FOLLOW THIS EXACTLY TO RUN TESTS**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test-core PROVIDER=<name> # Run core tests for a provider
|
||||||
|
make test-core PROVIDER=<name> TESTCASE=<X> # Run specific test
|
||||||
|
make test-core PROVIDER=<name> PATTERN=<X> # Run tests matching pattern
|
||||||
|
make test-mcp # Run all MCP tests
|
||||||
|
make test-mcp TYPE=<category> # Run MCP tests by category
|
||||||
|
make test-mcp TESTCASE=<TestName> # Run specific MCP test
|
||||||
|
make run-e2e FLOW=<feature> # Run E2E tests for feature
|
||||||
|
make run-e2e # Run all E2E tests
|
||||||
|
```
|
||||||
306
.claude/skills/resolve-pr-comments/SKILL.md
Normal file
306
.claude/skills/resolve-pr-comments/SKILL.md
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
name: resolve-pr-comments
|
||||||
|
description: Resolve all unresolved PR comments interactively. Makes local edits only—NEVER commits or pushes. Use when asked to resolve PR comments, address review feedback, handle CodeRabbit comments, or fix PR review issues. Invoked with /resolve-pr-comments <PR_NUMBER> or /resolve-pr-comments <owner/repo> <PR_NUMBER>.
|
||||||
|
allowed-tools: Read, Grep, Glob, Bash, Edit, Write, WebFetch, Task, AskUserQuestion, TodoWrite
|
||||||
|
---
|
||||||
|
|
||||||
|
# Resolve PR Comments
|
||||||
|
|
||||||
|
An interactive workflow to systematically address all unresolved PR review comments.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/resolve-pr-comments <PR_NUMBER>
|
||||||
|
/resolve-pr-comments <owner/repo> <PR_NUMBER>
|
||||||
|
```
|
||||||
|
|
||||||
|
If no repo is specified, uses the current git repository's remote origin.
|
||||||
|
|
||||||
|
**Before starting the workflow** - if the flow is in Plan Model - ask if the user wants to move to default mode to solve the comments one by one. Mention that each PR resolve has planning attached to it.
|
||||||
|
|
||||||
|
## Workflow Overview
|
||||||
|
|
||||||
|
1. **Detect repository** - Get owner/repo from git remote or user input
|
||||||
|
2. **Fetch unresolved comments** - Use GitHub GraphQL API (REST doesn't expose resolved status). Paginate through `reviewThreads` (cursor-based) so all pages are checked when a PR has more than 100 threads.
|
||||||
|
3. **Create tracking file** - Maintain state across the session
|
||||||
|
4. **For each comment**:
|
||||||
|
- Get full details and any existing replies
|
||||||
|
- Show the diff view of existing code in a proper diff view
|
||||||
|
- Before suggesting the fix - do the research via documentations. And present all that docs research and relevant links to the user with the fix. Use context 7. **MAKE SURE YOU DO THIS ALWAYS**
|
||||||
|
- Present to user with options (FIX, REPLY, SKIP)
|
||||||
|
- Wait for user decision
|
||||||
|
- Execute the action
|
||||||
|
- Update tracking
|
||||||
|
5. **Verify resolution** - Check remaining unresolved count
|
||||||
|
6. **Repeat until done** - Continue until all comments resolved
|
||||||
|
|
||||||
|
## Step 1: Detect Repository
|
||||||
|
|
||||||
|
If repository not provided, detect from git remote:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote get-url origin | sed -E 's|.*github.com[:/]([^/]+/[^/.]+)(\.git)?|\1|'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Fetch Unresolved Comments (GraphQL)
|
||||||
|
|
||||||
|
The REST API does NOT expose resolved/unresolved status. Use GraphQL.
|
||||||
|
|
||||||
|
**Important:** `reviewThreads` returns at most 100 threads per request. PRs with many review threads (e.g. large CodeRabbit reviews) need **pagination** or you will only see the first 100 threads and miss unresolved ones on later pages. Always paginate until `pageInfo.hasNextPage` is false so the count and list are complete.
|
||||||
|
|
||||||
|
### Single-page query (first 100 threads only)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh api graphql -f query='
|
||||||
|
{
|
||||||
|
repository(owner: "OWNER", name: "REPO") {
|
||||||
|
pullRequest(number: PR_NUMBER) {
|
||||||
|
reviewThreads(first: 100) {
|
||||||
|
pageInfo { hasNextPage endCursor }
|
||||||
|
nodes {
|
||||||
|
isResolved
|
||||||
|
comments(first: 1) {
|
||||||
|
nodes {
|
||||||
|
databaseId
|
||||||
|
path
|
||||||
|
body
|
||||||
|
author { login }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract unresolved (single page):
|
||||||
|
```bash
|
||||||
|
... | jq -r '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | "\(.comments.nodes[0].databaseId)|\(.comments.nodes[0].path)|\(.comments.nodes[0].author.login)"'
|
||||||
|
```
|
||||||
|
|
||||||
|
Avoid parsing `body` in the same jq pass if you paginate—comment bodies can contain control characters and break jq. Fetch full body per comment via REST when presenting (see Step 4).
|
||||||
|
|
||||||
|
### Paginate to collect all unresolved threads
|
||||||
|
|
||||||
|
Use cursor-based pagination so every thread is considered:
|
||||||
|
|
||||||
|
1. First request: `reviewThreads(first: 100)` (no `after`).
|
||||||
|
2. From the response: read `pageInfo.hasNextPage` and `pageInfo.endCursor`.
|
||||||
|
3. Next request: `reviewThreads(first: 100, after: $endCursor)`.
|
||||||
|
4. Append unresolved from this page to your list (id, path, author only).
|
||||||
|
5. Repeat from step 2 until `hasNextPage` is false.
|
||||||
|
|
||||||
|
Example loop (collects id|path|author for all unresolved; write to a file or variable):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CURSOR=""
|
||||||
|
while true; do
|
||||||
|
if [ -z "$CURSOR" ]; then
|
||||||
|
RESP=$(gh api graphql -f query='
|
||||||
|
query {
|
||||||
|
repository(owner: "OWNER", name: "REPO") {
|
||||||
|
pullRequest(number: PR_NUMBER) {
|
||||||
|
reviewThreads(first: 100) {
|
||||||
|
pageInfo { hasNextPage endCursor }
|
||||||
|
nodes {
|
||||||
|
isResolved
|
||||||
|
comments(first: 1) {
|
||||||
|
nodes { databaseId path author { login } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}')
|
||||||
|
else
|
||||||
|
RESP=$(gh api graphql -f query='
|
||||||
|
query($after: String) {
|
||||||
|
repository(owner: "OWNER", name: "REPO") {
|
||||||
|
pullRequest(number: PR_NUMBER) {
|
||||||
|
reviewThreads(first: 100, after: $after) {
|
||||||
|
pageInfo { hasNextPage endCursor }
|
||||||
|
nodes {
|
||||||
|
isResolved
|
||||||
|
comments(first: 1) {
|
||||||
|
nodes { databaseId path author { login } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' -f after="$CURSOR")
|
||||||
|
fi
|
||||||
|
# Append unresolved from this page (id|path|author only; no body to avoid control chars)
|
||||||
|
echo "$RESP" | jq -r '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[0] | "\(.databaseId)|\(.path)|\(.author.login)"'
|
||||||
|
HAS_NEXT=$(echo "$RESP" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage')
|
||||||
|
CURSOR=$(echo "$RESP" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor // ""')
|
||||||
|
[ "$HAS_NEXT" != "true" ] || [ -z "$CURSOR" ] && break
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Total unresolved count = number of lines output. Use the comment `databaseId` values to fetch full body via REST when presenting each comment (Step 4).
|
||||||
|
|
||||||
|
## Step 3: Create Tracking File
|
||||||
|
|
||||||
|
Create at `/tmp/pr-review/pr-<NUMBER>-comments.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# PR #<NUMBER> Comment Review (<owner>/<repo>)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
- Total unresolved: <count>
|
||||||
|
- Fixed: 0
|
||||||
|
- Replied: 0
|
||||||
|
- Skipped: 0
|
||||||
|
|
||||||
|
## Comments to Address
|
||||||
|
| # | ID | File | Issue | Status |
|
||||||
|
|---|-----|------|-------|--------|
|
||||||
|
| 1 | 12345 | src/foo.ts | Missing validation | pending |
|
||||||
|
|
||||||
|
## Actions Taken
|
||||||
|
| ID | Action | Details |
|
||||||
|
|----|--------|---------|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Present Each Comment
|
||||||
|
|
||||||
|
For each unresolved comment, present in this format:
|
||||||
|
|
||||||
|
```
|
||||||
|
**Comment #<N>/<TOTAL>: ID <ID> - <File>**
|
||||||
|
|
||||||
|
**What it says:**
|
||||||
|
<Summary of the comment's concern>
|
||||||
|
|
||||||
|
**Current code state:**
|
||||||
|
<Show relevant code snippet if applicable - READ THE FILE>
|
||||||
|
|
||||||
|
**Documentations referred**
|
||||||
|
For anything related to LLM calls (in /core module) - make sure you refer to the documentation. You have access to web_search and context7. and show that too
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
1. **FIX** - <Describe what the fix would be>
|
||||||
|
2. **REPLY** - <Describe the reply explaining why no fix needed>
|
||||||
|
3. **SKIP** - Move on without action
|
||||||
|
|
||||||
|
**My recommendation:** <OPTION> - <Brief reasoning>
|
||||||
|
|
||||||
|
Go ahead?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting Full Comment Details
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh api repos/OWNER/REPO/pulls/PR_NUMBER/comments --paginate | jq -r '.[] | select(.id == COMMENT_ID) | .body'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checking for Existing Replies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh api repos/OWNER/REPO/pulls/PR_NUMBER/comments --paginate | jq '.[] | select(.id == COMMENT_ID or .in_reply_to_id == COMMENT_ID) | {id, user: .user.login, body: (.body | gsub("\n"; " ") | .[0:150])}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5: Execute Actions
|
||||||
|
|
||||||
|
**CRITICAL: Do NOT reply to PR comments until changes are pushed to the remote.** The reviewer cannot verify fixes until the code is pushed. Collect all fixes locally. This skill NEVER commits or pushes—the user handles that manually.
|
||||||
|
|
||||||
|
### For FIX:
|
||||||
|
1. Make the code change using Edit tool
|
||||||
|
2. Before applying the changes take approval from the user. DO NOT DIRECTLY MAKE CHANGE BEFORE user says yes. Also give an option to suggest the changes to code.
|
||||||
|
3. Track the fix locally in the tracking file (do NOT reply yet)
|
||||||
|
4. Continue to next comment
|
||||||
|
|
||||||
|
### For REPLY (non-code responses like "out of scope", "intentional design"):
|
||||||
|
These can be posted immediately since they don't require code verification. Use the **replies** endpoint only (see below).
|
||||||
|
|
||||||
|
## Reply endpoint (use this only)
|
||||||
|
|
||||||
|
To reply to a review comment, use the dedicated replies endpoint. **Do not** use `POST .../pulls/PR_NUMBER/comments` with `in_reply_to` — that returns 422 (in_reply_to is not a permitted key for create review comment).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh api repos/OWNER/REPO/pulls/PR_NUMBER/comments/COMMENT_ID/replies -X POST -f body="<your reply>"
|
||||||
|
```
|
||||||
|
|
||||||
|
- `COMMENT_ID` is the numeric comment id (same as GraphQL `databaseId` from the thread's first comment).
|
||||||
|
- Request body: only `body` (string). No `in_reply_to`, `commit_id`, or path params.
|
||||||
|
|
||||||
|
## Step 5b: Push and Reply to FIX comments
|
||||||
|
|
||||||
|
After ALL comments have been addressed locally:
|
||||||
|
|
||||||
|
1. Ask user if they have pushed these changes to remote. Yes/No
|
||||||
|
2. **Only after push succeeds**, reply to each FIX comment using the replies endpoint:
|
||||||
|
```bash
|
||||||
|
gh api repos/OWNER/REPO/pulls/PR_NUMBER/comments/COMMENT_ID/replies -X POST -f body="Fixed - <description of change>. See updated code."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch workflow (fix all → push → post all)
|
||||||
|
|
||||||
|
If the user says e.g. "resolve all comments then push then post", you may:
|
||||||
|
1. Apply all FIX and REPLY decisions locally (with user approval per comment or bulk approval).
|
||||||
|
2. Ask user to push.
|
||||||
|
3. After push, post all replies in sequence: FIX replies first, then any REPLY-only replies, using the same `.../comments/COMMENT_ID/replies` endpoint for each.
|
||||||
|
|
||||||
|
### Common Reply Templates
|
||||||
|
|
||||||
|
**Out of scope:**
|
||||||
|
```
|
||||||
|
This is a valid improvement but out of scope for this PR. Tracked for future work.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Already addressed:**
|
||||||
|
```
|
||||||
|
Already addressed - <variable/file> now has <fix>. See line <N>.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Intentional design:**
|
||||||
|
```
|
||||||
|
This is intentional. <Explanation of why the current approach is correct>.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Different module:**
|
||||||
|
```
|
||||||
|
This comment refers to <module> which is a different module not modified in this PR. It's working as-is.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Asking bot to verify:**
|
||||||
|
```
|
||||||
|
This is solved, can you check and resolve if done properly?
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 6: Verify Resolution
|
||||||
|
|
||||||
|
After addressing comments, check remaining unresolved count. If the PR has more than 100 review threads, use the same pagination loop as in Step 2 and count unresolved across all pages; a single-page query only sees the first 100 threads.
|
||||||
|
|
||||||
|
Single-page check (first 100 threads only):
|
||||||
|
```bash
|
||||||
|
gh api graphql -f query='...' | jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length'
|
||||||
|
```
|
||||||
|
|
||||||
|
If count is 0 (across all pages), report success. If comments remain:
|
||||||
|
- Some bots (like CodeRabbit) take time to auto-resolve
|
||||||
|
- User may need to push code changes first
|
||||||
|
- Re-run the workflow to address remaining comments
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
1. **NEVER commit or push changes** - This skill only makes local edits. The user handles `git add`, `git commit`, and `git push` themselves. Do not run any git commit or git push commands.
|
||||||
|
2. **NEVER reply "Fixed" until code is pushed** - The reviewer cannot verify fixes until they're on the remote. Make all fixes locally. Only reply to FIX comments after the user confirms they have pushed (the user pushes manually).
|
||||||
|
3. **Always read the file** before suggesting fixes - understand context
|
||||||
|
4. **Check for existing replies** in the thread before responding
|
||||||
|
5. **Wait for user approval** on each action - never auto-fix without confirmation
|
||||||
|
6. **Update tracking file** after each action
|
||||||
|
7. **Some bots are slow** - CodeRabbit may take minutes to auto-resolve after push
|
||||||
|
8. **User pushes manually** - This skill never commits or pushes; the user must push code changes before expecting auto-resolution of FIX actions
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- If `gh` not authenticated: `gh auth login`
|
||||||
|
- If repo not found: verify owner/repo spelling
|
||||||
|
- If PR not found: verify PR number exists
|
||||||
|
- If comment ID invalid: re-fetch unresolved comments (may have been resolved)
|
||||||
|
- If reply returns 422 "in_reply_to is not a permitted key": you are using the wrong endpoint. Use `POST .../pulls/PR_NUMBER/comments/COMMENT_ID/replies` with only `-f body="..."`, not the create-comment endpoint.
|
||||||
87
.dockerignore
Normal file
87
.dockerignore
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.github/
|
||||||
|
|
||||||
|
# Node.js
|
||||||
|
node_modules/
|
||||||
|
**/node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.env
|
||||||
|
|
||||||
|
# IDE and editor files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
|
||||||
|
# Cache directories
|
||||||
|
.cache/
|
||||||
|
.parcel-cache/
|
||||||
|
|
||||||
|
# Documentation that's not needed for build
|
||||||
|
docs/
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# CI/CD
|
||||||
|
ci/
|
||||||
|
|
||||||
|
# Plugin build artifacts
|
||||||
|
plugins/*/dist/
|
||||||
|
|
||||||
|
# Test directories
|
||||||
|
tests/
|
||||||
|
test/
|
||||||
|
__tests__/
|
||||||
|
|
||||||
|
# Not needed for build
|
||||||
|
cli/
|
||||||
|
terraform/
|
||||||
|
helm-charts/
|
||||||
|
npx/
|
||||||
|
nix/
|
||||||
|
recipes/
|
||||||
|
examples/
|
||||||
|
.claude/
|
||||||
|
pulse.yaml
|
||||||
|
flake.nix
|
||||||
|
|
||||||
|
# Temporary build artifacts
|
||||||
|
tmp/
|
||||||
|
**/tmp/
|
||||||
|
transports/bifrost-http/tmp/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
.tmp/
|
||||||
|
|
||||||
|
# Go workspaces (local only)
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
13
.editorconfig
Normal file
13
.editorconfig
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
insert_final_newline = false
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.{js,jsx,ts,tsx,mjs,json,md,css,scss,html}]
|
||||||
|
insert_final_newline = false
|
||||||
12
.gitattributes
vendored
Normal file
12
.gitattributes
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Ensure shell scripts always use LF line endings
|
||||||
|
*.sh text eol=lf
|
||||||
|
|
||||||
|
# Ensure Docker entrypoint uses LF
|
||||||
|
docker-entrypoint.sh text eol=lf
|
||||||
|
|
||||||
|
# Default behavior for all other files
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# Mark dummy credentials as generated to avoid security scanner false positives
|
||||||
|
tests/integrations/dummy-gcp-credentials.json linguist-generated=true
|
||||||
|
|
||||||
9
.github/CODEOWNERS
vendored
Normal file
9
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.github/ @maximhq/bifrost-admin
|
||||||
|
.claude/ @maximhq/bifrost-admin
|
||||||
|
.nix/ @maximhq/bifrost-admin
|
||||||
|
Makefile @maximhq/bifrost-admin
|
||||||
|
flake.nix @maximhq/bifrost-admin
|
||||||
|
tests/scripts/ @maximhq/bifrost-admin
|
||||||
|
*.sh @maximhq/bifrost-admin
|
||||||
|
helm-charts/ @maximhq/bifrost-admin
|
||||||
|
terraform/ @maximhq/bifrost-admin
|
||||||
131
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
131
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
name: Bug report
|
||||||
|
description: Report a problem or regression in Bifrost
|
||||||
|
title: "[Bug]: <short summary>"
|
||||||
|
labels: [bug]
|
||||||
|
assignees: []
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out a bug report! Please provide as much detail as possible.
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: prerequisites
|
||||||
|
attributes:
|
||||||
|
label: Prerequisites
|
||||||
|
options:
|
||||||
|
- label: I have searched existing issues and discussions to avoid duplicates
|
||||||
|
required: true
|
||||||
|
- label: I am using the latest version (or have tested against main/nightly)
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: What happened? Include screenshots if helpful.
|
||||||
|
placeholder: Clear and concise description of the bug
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Provide a minimal, reproducible example. Link to a repo, gist, or include exact steps.
|
||||||
|
placeholder: |
|
||||||
|
1. Go to '...'
|
||||||
|
2. Run '...'
|
||||||
|
3. Observe '...'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
placeholder: What did you expect to happen?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: actual
|
||||||
|
attributes:
|
||||||
|
label: Actual behavior
|
||||||
|
placeholder: What actually happened?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: area
|
||||||
|
attributes:
|
||||||
|
label: Affected area(s)
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Core (Go)
|
||||||
|
- Framework
|
||||||
|
- Transports (HTTP)
|
||||||
|
- Plugins
|
||||||
|
- UI (React)
|
||||||
|
- Docs
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Version
|
||||||
|
description: Affected version(s).
|
||||||
|
placeholder: e.g., v1.0.3
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: env
|
||||||
|
attributes:
|
||||||
|
label: Environment
|
||||||
|
description: Include as many as apply.
|
||||||
|
placeholder: |
|
||||||
|
- OS: macOS 14.5, Linux x.y, Windows 11
|
||||||
|
- Go: 1.22.x
|
||||||
|
- Node: 20.x, npm/pnpm/yarn version
|
||||||
|
- Browser (if UI): Chrome/Firefox/Safari versions
|
||||||
|
- Bifrost components and versions (core, transports, ui)
|
||||||
|
- Any relevant environment flags/config
|
||||||
|
render: text
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Relevant logs/output
|
||||||
|
description: Paste error logs, stack traces, or console output.
|
||||||
|
render: shell
|
||||||
|
placeholder: |
|
||||||
|
<paste logs here>
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: regression
|
||||||
|
attributes:
|
||||||
|
label: Regression?
|
||||||
|
description: If this worked in a previous version, which version?
|
||||||
|
placeholder: e.g., Worked in v0.8.0, broke in v0.9.0
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: severity
|
||||||
|
attributes:
|
||||||
|
label: Severity
|
||||||
|
options:
|
||||||
|
- Low (minor issue or cosmetic)
|
||||||
|
- Medium (some functionality impaired)
|
||||||
|
- High (major functionality broken)
|
||||||
|
- Critical (blocks releases or production)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
|
||||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
2
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
|
||||||
45
.github/ISSUE_TEMPLATE/docs_issue.yml
vendored
Normal file
45
.github/ISSUE_TEMPLATE/docs_issue.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: Documentation issue
|
||||||
|
description: Report missing, unclear, or incorrect documentation
|
||||||
|
title: "[Docs]: <short summary>"
|
||||||
|
labels: [documentation]
|
||||||
|
assignees: []
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Help us improve the docs! Please provide links and suggestions.
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: prerequisites
|
||||||
|
attributes:
|
||||||
|
label: Prerequisites
|
||||||
|
options:
|
||||||
|
- label: I have searched existing issues and docs to avoid duplicates
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: page
|
||||||
|
attributes:
|
||||||
|
label: Affected page(s)
|
||||||
|
description: Provide the path or URL to the affected doc(s)
|
||||||
|
placeholder: docs/usage/providers.md or https://...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: issue
|
||||||
|
attributes:
|
||||||
|
label: What’s wrong or missing?
|
||||||
|
description: Be as specific as possible.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: suggestion
|
||||||
|
attributes:
|
||||||
|
label: Suggested change
|
||||||
|
description: Propose wording or structure improvements.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
|
||||||
69
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
69
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
name: Feature request
|
||||||
|
description: Suggest an idea or enhancement for Bifrost
|
||||||
|
title: "[Feature]: <short summary>"
|
||||||
|
labels: [enhancement]
|
||||||
|
assignees: []
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for proposing a feature! Please fill out the details below.
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: prerequisites
|
||||||
|
attributes:
|
||||||
|
label: Prerequisites
|
||||||
|
options:
|
||||||
|
- label: I have searched existing issues and discussions to avoid duplicates
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Problem to solve
|
||||||
|
description: What problem does this feature solve? Who benefits?
|
||||||
|
placeholder: Describe the problem clearly.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: proposal
|
||||||
|
attributes:
|
||||||
|
label: Proposed solution
|
||||||
|
description: Describe your proposed API/UX/CLI. Include examples if helpful.
|
||||||
|
placeholder: Provide details about how this should work.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Alternatives considered
|
||||||
|
description: What other solutions or workarounds did you consider?
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: area
|
||||||
|
attributes:
|
||||||
|
label: Area(s)
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Core (Go)
|
||||||
|
- Framework
|
||||||
|
- Transports (HTTP)
|
||||||
|
- Plugins
|
||||||
|
- UI (React)
|
||||||
|
- Docs
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add any other context, sketches, or references here.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
|
||||||
BIN
.github/assets/bifrost-logo.png
vendored
Normal file
BIN
.github/assets/bifrost-logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
.github/assets/book-demo-button.png
vendored
Normal file
BIN
.github/assets/book-demo-button.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
.github/assets/features.png
vendored
Normal file
BIN
.github/assets/features.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 189 KiB |
266
.github/dependabot.yml
vendored
Normal file
266
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: "/transports"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
# Go / npm / Rust (daily). Docker + GitHub Actions: weekly entries above.
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /cli
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /core
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /examples/mcps/auth-demo-server
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /examples/mcps/edge-case-server
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /examples/mcps/edge-case-server
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /examples/mcps/error-test-server
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /examples/mcps/error-test-server
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /examples/mcps/go-test-server
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /examples/mcps/http-no-ping-server
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /examples/mcps/parallel-test-server
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /examples/mcps/parallel-test-server
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /examples/mcps/temperature
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /examples/mcps/test-tools-server
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /examples/plugins/hello-world-wasm-go
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: cargo
|
||||||
|
directory: /examples/plugins/hello-world-wasm-rust
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /examples/plugins/hello-world-wasm-typescript
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /examples/plugins/hello-world
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /examples/plugins/http-transport-only
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /examples/plugins/llm-only
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /examples/plugins/mcp-only
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /examples/plugins/multi-interface
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /framework
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /npx/bifrost-cli
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /npx/bifrost
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /plugins/governance
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /plugins/jsonparser
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /plugins/litellmcompat
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /plugins/logging
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /plugins/maxim
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /plugins/mocker
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /plugins/otel
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /plugins/semanticcache
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /plugins/telemetry
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /tests/e2e/api/newman-reporter-dbverify
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /tests/e2e/api
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /tests/e2e
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /tests/governance
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /tests/integrations/typescript
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /tests/scripts/1millogs
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /tests/scripts/migration-checker
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /transports
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /ui
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 0
|
||||||
72
.github/pull_request_template.md
vendored
Normal file
72
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
## Summary
|
||||||
|
|
||||||
|
Briefly explain the purpose of this PR and the problem it solves.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- What was changed and why
|
||||||
|
- Any notable design decisions or trade-offs
|
||||||
|
|
||||||
|
## Type of change
|
||||||
|
|
||||||
|
- [ ] Bug fix
|
||||||
|
- [ ] Feature
|
||||||
|
- [ ] Refactor
|
||||||
|
- [ ] Documentation
|
||||||
|
- [ ] Chore/CI
|
||||||
|
|
||||||
|
## Affected areas
|
||||||
|
|
||||||
|
- [ ] Core (Go)
|
||||||
|
- [ ] Transports (HTTP)
|
||||||
|
- [ ] Providers/Integrations
|
||||||
|
- [ ] Plugins
|
||||||
|
- [ ] UI (React)
|
||||||
|
- [ ] Docs
|
||||||
|
|
||||||
|
## How to test
|
||||||
|
|
||||||
|
Describe the steps to validate this change. Include commands and expected outcomes.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Core/Transports
|
||||||
|
go version
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# UI
|
||||||
|
cd ui
|
||||||
|
pnpm i || npm i
|
||||||
|
pnpm test || npm test
|
||||||
|
pnpm build || npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
If adding new configs or environment variables, document them here.
|
||||||
|
|
||||||
|
## Screenshots/Recordings
|
||||||
|
|
||||||
|
If UI changes, add before/after screenshots or short clips.
|
||||||
|
|
||||||
|
## Breaking changes
|
||||||
|
|
||||||
|
- [ ] Yes
|
||||||
|
- [ ] No
|
||||||
|
|
||||||
|
If yes, describe impact and migration instructions.
|
||||||
|
|
||||||
|
## Related issues
|
||||||
|
|
||||||
|
Link related issues and discussions. Example: Closes #123
|
||||||
|
|
||||||
|
## Security considerations
|
||||||
|
|
||||||
|
Note any security implications (auth, secrets, PII, sandboxing, etc.).
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] I read `docs/contributing/README.md` and followed the guidelines
|
||||||
|
- [ ] I added/updated tests where appropriate
|
||||||
|
- [ ] I updated documentation where needed
|
||||||
|
- [ ] I verified builds succeed (Go and UI)
|
||||||
|
- [ ] I verified the CI pipeline passes locally if applicable
|
||||||
|
|
||||||
|
|
||||||
0
.github/workflows/configs/default/.gitkeep
vendored
Normal file
0
.github/workflows/configs/default/.gitkeep
vendored
Normal file
57
.github/workflows/configs/default/config.json
vendored
Normal file
57
.github/workflows/configs/default/config.json
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://www.getbifrost.ai/schema",
|
||||||
|
"config_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "postgres",
|
||||||
|
"config": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": "5432",
|
||||||
|
"user": "bifrost",
|
||||||
|
"password": "bifrost_password",
|
||||||
|
"db_name": "bifrost",
|
||||||
|
"ssl_mode": "disable"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logs_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "postgres",
|
||||||
|
"config": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": "5432",
|
||||||
|
"user": "bifrost",
|
||||||
|
"password": "bifrost_password",
|
||||||
|
"db_name": "bifrost",
|
||||||
|
"ssl_mode": "disable"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"providers": {
|
||||||
|
"openai": {
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"name": "e2e-openai-key",
|
||||||
|
"value": "env.OPENAI_API_KEY",
|
||||||
|
"weight": 1,
|
||||||
|
"models": ["*"],
|
||||||
|
"use_for_batch_api": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"network_config": {
|
||||||
|
"default_request_timeout_in_seconds": 300
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"anthropic": {
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"name": "e2e-anthropic-key",
|
||||||
|
"value": "env.ANTHROPIC_API_KEY",
|
||||||
|
"weight": 1,
|
||||||
|
"models": ["*"],
|
||||||
|
"use_for_batch_api": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"network_config": {
|
||||||
|
"default_request_timeout_in_seconds": 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
107
.github/workflows/configs/docker-compose.yml
vendored
Normal file
107
.github/workflows/configs/docker-compose.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: bifrost
|
||||||
|
POSTGRES_PASSWORD: bifrost_password
|
||||||
|
POSTGRES_DB: bifrost
|
||||||
|
PGDATA: /var/lib/postgresql/data/pgdata
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U bifrost -d bifrost"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- bifrost_network
|
||||||
|
|
||||||
|
weaviate:
|
||||||
|
image: cr.weaviate.io/semitechnologies/weaviate:1.32.4
|
||||||
|
command:
|
||||||
|
- --host
|
||||||
|
- 0.0.0.0
|
||||||
|
- --port
|
||||||
|
- '8080'
|
||||||
|
- --scheme
|
||||||
|
- http
|
||||||
|
environment:
|
||||||
|
- CLUSTER_HOSTNAME=weaviate
|
||||||
|
- CLUSTER_ADVERTISE_ADDR=172.38.0.12
|
||||||
|
- CLUSTER_GOSSIP_BIND_PORT=7946
|
||||||
|
- CLUSTER_DATA_BIND_PORT=7947
|
||||||
|
- DISABLE_TELEMETRY=true
|
||||||
|
- PERSISTENCE_DATA_PATH=/var/lib/weaviate
|
||||||
|
- DEFAULT_VECTORIZER_MODULE=none
|
||||||
|
- ENABLE_MODULES=
|
||||||
|
- AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
ports:
|
||||||
|
- "9000:8080"
|
||||||
|
volumes:
|
||||||
|
- weaviate_data:/var/lib/weaviate
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/v1/.well-known/ready"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
bifrost_network:
|
||||||
|
ipv4_address: 172.38.0.12
|
||||||
|
|
||||||
|
# Redis Stack instance for vector store tests
|
||||||
|
redis-stack:
|
||||||
|
image: redis/redis-stack:7.4.0-v6
|
||||||
|
command: redis-stack-server --protected-mode no
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
- "8001:8001" # RedisInsight web UI
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
bifrost_network:
|
||||||
|
ipv4_address: 172.38.0.13
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Qdrant instance for vector store tests
|
||||||
|
qdrant:
|
||||||
|
image: qdrant/qdrant:v1.16.0
|
||||||
|
ports:
|
||||||
|
- "6333:6333" # REST API
|
||||||
|
- "6334:6334" # gRPC API
|
||||||
|
volumes:
|
||||||
|
- qdrant_data:/qdrant/storage
|
||||||
|
networks:
|
||||||
|
bifrost_network:
|
||||||
|
ipv4_address: 172.38.0.14
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/6333'"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
networks:
|
||||||
|
bifrost_network:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.38.0.0/16
|
||||||
|
gateway: 172.38.0.1
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
driver: local
|
||||||
|
weaviate_data:
|
||||||
|
driver: local
|
||||||
|
redis_data:
|
||||||
|
driver: local
|
||||||
|
qdrant_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
0
.github/workflows/configs/emptystate/.gitkeep
vendored
Normal file
0
.github/workflows/configs/emptystate/.gitkeep
vendored
Normal file
9
.github/workflows/configs/noconfigstorenologstore/config.json
vendored
Normal file
9
.github/workflows/configs/noconfigstorenologstore/config.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://www.getbifrost.ai/schema",
|
||||||
|
"config_store": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"logs_store": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
27
.github/workflows/configs/witconfigstorelogstorepostgres/config.json
vendored
Normal file
27
.github/workflows/configs/witconfigstorelogstorepostgres/config.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://www.getbifrost.ai/schema",
|
||||||
|
"config_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "postgres",
|
||||||
|
"config": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": "5432",
|
||||||
|
"user": "bifrost",
|
||||||
|
"password": "bifrost_password",
|
||||||
|
"db_name": "bifrost",
|
||||||
|
"ssl_mode": "disable"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logs_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "postgres",
|
||||||
|
"config": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": "5432",
|
||||||
|
"user": "bifrost",
|
||||||
|
"password": "bifrost_password",
|
||||||
|
"db_name": "bifrost",
|
||||||
|
"ssl_mode": "disable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
.github/workflows/configs/withconfigstore/config.json
vendored
Normal file
10
.github/workflows/configs/withconfigstore/config.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://www.getbifrost.ai/schema",
|
||||||
|
"config_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "sqlite",
|
||||||
|
"config": {
|
||||||
|
"path": "../.github/workflows/configs/withconfigstore/config.db"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
.github/workflows/configs/withconfigstorelogsstorepostgres/config.json
vendored
Normal file
27
.github/workflows/configs/withconfigstorelogsstorepostgres/config.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://www.getbifrost.ai/schema",
|
||||||
|
"config_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "postgres",
|
||||||
|
"config": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": "5432",
|
||||||
|
"user": "bifrost",
|
||||||
|
"password": "bifrost_password",
|
||||||
|
"db_name": "bifrost",
|
||||||
|
"ssl_mode": "disable"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logs_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "postgres",
|
||||||
|
"config": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": "5432",
|
||||||
|
"user": "bifrost",
|
||||||
|
"password": "bifrost_password",
|
||||||
|
"db_name": "bifrost",
|
||||||
|
"ssl_mode": "disable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
.github/workflows/configs/withconfigstorelogsstoresqlite/config.json
vendored
Normal file
17
.github/workflows/configs/withconfigstorelogsstoresqlite/config.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://www.getbifrost.ai/schema",
|
||||||
|
"config_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "sqlite",
|
||||||
|
"config": {
|
||||||
|
"path": "../.github/workflows/configs/withconfigstorelogsstoresqlite/config.db"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logs_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "sqlite",
|
||||||
|
"config": {
|
||||||
|
"path": "../.github/workflows/configs/withconfigstorelogsstoresqlite/logs.db"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
.github/workflows/configs/withdynamicplugin/config.json
vendored
Normal file
17
.github/workflows/configs/withdynamicplugin/config.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://www.getbifrost.ai/schema",
|
||||||
|
"config_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "sqlite",
|
||||||
|
"config": {
|
||||||
|
"path": "../.github/workflows/configs/withdynamicplugin/config.db"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"name": "hello-world",
|
||||||
|
"path": "../examples/plugins/hello-world/build/hello-world.so"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
29
.github/workflows/configs/withobservability/config.json
vendored
Normal file
29
.github/workflows/configs/withobservability/config.json
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://www.getbifrost.ai/schema",
|
||||||
|
"config_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "sqlite",
|
||||||
|
"config": {
|
||||||
|
"path": "../.github/workflows/configs/withobservability/config.db"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logs_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "sqlite",
|
||||||
|
"config": {
|
||||||
|
"path": "../.github/workflows/configs/withobservability/logs.db"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"name": "otel",
|
||||||
|
"config": {
|
||||||
|
"service_name": "bifrost",
|
||||||
|
"collector_url": "http://localhost:4318/v1/traces",
|
||||||
|
"trace_type": "genai_extension",
|
||||||
|
"protocol": "http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
150
.github/workflows/configs/withpostgresmcpclientsinconfig/config.json
vendored
Normal file
150
.github/workflows/configs/withpostgresmcpclientsinconfig/config.json
vendored
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://www.getbifrost.ai/schema",
|
||||||
|
"client": {
|
||||||
|
"allow_direct_keys": false,
|
||||||
|
"allowed_origins": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"disable_content_logging": false,
|
||||||
|
"drop_excess_requests": false,
|
||||||
|
"enable_logging": true,
|
||||||
|
"enforce_auth_on_inference": true,
|
||||||
|
"initial_pool_size": 300,
|
||||||
|
"log_retention_days": 365,
|
||||||
|
"max_request_body_size_mb": 100
|
||||||
|
},
|
||||||
|
"config_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "postgres",
|
||||||
|
"config": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": "5432",
|
||||||
|
"user": "bifrost",
|
||||||
|
"password": "bifrost_password",
|
||||||
|
"db_name": "bifrost",
|
||||||
|
"ssl_mode": "disable"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logs_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "postgres",
|
||||||
|
"config": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": "5432",
|
||||||
|
"user": "bifrost",
|
||||||
|
"password": "bifrost_password",
|
||||||
|
"db_name": "bifrost",
|
||||||
|
"ssl_mode": "disable"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mcp": {
|
||||||
|
"client_configs": [
|
||||||
|
{
|
||||||
|
"name": "WeatherService",
|
||||||
|
"connection_type": "http",
|
||||||
|
"client_id": "weather-mcp-server",
|
||||||
|
"connection_string": "http://localhost:8080/mcp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CalendarService",
|
||||||
|
"connection_type": "http",
|
||||||
|
"client_id": "calendar-mcp-server",
|
||||||
|
"connection_string": "http://localhost:8081/mcp"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"governance": {
|
||||||
|
"auth_config": {
|
||||||
|
"admin_password": "env.BIFROST_ADMIN_PASSWORD",
|
||||||
|
"admin_username": "env.BIFROST_ADMIN_USERNAME",
|
||||||
|
"disable_auth_on_inference": true,
|
||||||
|
"is_enabled": false
|
||||||
|
},
|
||||||
|
"virtual_keys": [
|
||||||
|
{
|
||||||
|
"id": "vk-ai-portal-prod",
|
||||||
|
"is_active": true,
|
||||||
|
"name": "ai-portal-production-key",
|
||||||
|
"description": "Virtual key for AI portal with MCP access to weather and calendar services",
|
||||||
|
"value": "env.BIFROST_VK_AI_PORTAL",
|
||||||
|
"mcp_configs": [
|
||||||
|
{
|
||||||
|
"mcp_client_name": "WeatherService",
|
||||||
|
"tools_to_execute": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mcp_client_name": "CalendarService",
|
||||||
|
"tools_to_execute": [
|
||||||
|
"get_events",
|
||||||
|
"create_event"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"provider_configs": [
|
||||||
|
{
|
||||||
|
"provider": "openai",
|
||||||
|
"allowed_models": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"key_ids": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"weight": 1.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "vk-internal-tools",
|
||||||
|
"is_active": true,
|
||||||
|
"name": "internal-tools-key",
|
||||||
|
"description": "Virtual key for internal tools with limited MCP access",
|
||||||
|
"value": "env.BIFROST_VK_INTERNAL",
|
||||||
|
"mcp_configs": [
|
||||||
|
{
|
||||||
|
"mcp_client_name": "WeatherService",
|
||||||
|
"tools_to_execute": [
|
||||||
|
"get_current_weather"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"provider_configs": [
|
||||||
|
{
|
||||||
|
"provider": "openai",
|
||||||
|
"allowed_models": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"key_ids": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"weight": 1.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"is_vk_mandatory": true
|
||||||
|
},
|
||||||
|
"enabled": true,
|
||||||
|
"name": "governance"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"providers": {
|
||||||
|
"openai": {
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"name": "openai-primary",
|
||||||
|
"value": "env.OPENAI_API_KEY",
|
||||||
|
"weight": 1,
|
||||||
|
"models": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
.github/workflows/configs/withsemanticcache/config.json
vendored
Normal file
21
.github/workflows/configs/withsemanticcache/config.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://www.getbifrost.ai/schema",
|
||||||
|
"vector_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "weaviate",
|
||||||
|
"config": {
|
||||||
|
"scheme": "http",
|
||||||
|
"host": "localhost:9000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"name": "semantic_cache",
|
||||||
|
"config": {
|
||||||
|
"dimension": 1,
|
||||||
|
"vector_store_namespace": "test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
61
.github/workflows/dependabot-alerts.yml
vendored
Normal file
61
.github/workflows/dependabot-alerts.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
name: Dependabot Alerts to Issues
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 9 * * 1" # Weekly on Monday at 9am UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-issues:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: block
|
||||||
|
allowed-endpoints: >
|
||||||
|
api.github.com:443
|
||||||
|
|
||||||
|
- name: Create issues from Dependabot alerts
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
alerts=$(gh api repos/${{ github.repository }}/dependabot/alerts \
|
||||||
|
--jq '[.[] | select(.state == "open")]')
|
||||||
|
|
||||||
|
echo "$alerts" | jq -c '.[]' | while read -r alert; do
|
||||||
|
pkg=$(echo "$alert" | jq -r '.dependency.package.name')
|
||||||
|
number=$(echo "$alert" | jq -r '.number')
|
||||||
|
severity=$(echo "$alert" | jq -r '.security_advisory.severity')
|
||||||
|
summary=$(echo "$alert" | jq -r '.security_advisory.summary')
|
||||||
|
url=$(echo "$alert" | jq -r '.html_url')
|
||||||
|
ecosystem=$(echo "$alert" | jq -r '.dependency.package.ecosystem')
|
||||||
|
|
||||||
|
# Skip if issue already exists for this alert
|
||||||
|
existing=$(gh issue list \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--search "Dependabot Alert #${number}" \
|
||||||
|
--json number --jq 'length')
|
||||||
|
|
||||||
|
if [ "$existing" = "0" ]; then
|
||||||
|
gh issue create \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--title "dep: update ${pkg} (${severity})" \
|
||||||
|
--label "dependencies" \
|
||||||
|
--body "$(cat <<EOF
|
||||||
|
## Dependabot Alert #${number}
|
||||||
|
|
||||||
|
**Package:** \`${pkg}\`
|
||||||
|
**Ecosystem:** ${ecosystem}
|
||||||
|
**Severity:** ${severity}
|
||||||
|
|
||||||
|
${summary}
|
||||||
|
|
||||||
|
[View Alert](${url})
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
32
.github/workflows/dependency-review.yml
vendored
Normal file
32
.github/workflows/dependency-review.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Dependency Review Action
|
||||||
|
#
|
||||||
|
# This Action will scan dependency manifest files that change as part of a Pull Request,
|
||||||
|
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
|
||||||
|
# Once installed, if the workflow run is marked as required,
|
||||||
|
# PRs introducing known-vulnerable packages will be blocked from merging.
|
||||||
|
#
|
||||||
|
# Source repository: https://github.com/actions/dependency-review-action
|
||||||
|
name: 'Dependency Review'
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dependency-review:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: block
|
||||||
|
allowed-endpoints: >
|
||||||
|
api.deps.dev:443
|
||||||
|
api.github.com:443
|
||||||
|
api.securityscorecards.dev:443
|
||||||
|
github.com:443
|
||||||
|
|
||||||
|
- name: 'Checkout Repository'
|
||||||
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||||
|
- name: 'Dependency Review'
|
||||||
|
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
|
||||||
46
.github/workflows/docs-validation.yml
vendored
Normal file
46
.github/workflows/docs-validation.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: Docs Validation
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "docs/**"
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
paths:
|
||||||
|
- "docs/**"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-broken-links:
|
||||||
|
name: Check Broken Links
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: block
|
||||||
|
allowed-endpoints: >
|
||||||
|
api.github.com:443
|
||||||
|
github.com:443
|
||||||
|
nodejs.org:443
|
||||||
|
ph.mintlify.com:443
|
||||||
|
registry.npmjs.org:443
|
||||||
|
release-assets.githubusercontent.com:443
|
||||||
|
storage.googleapis.com:443
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version: "21"
|
||||||
|
|
||||||
|
- name: Check for broken links
|
||||||
|
working-directory: ./docs
|
||||||
|
run: |
|
||||||
|
echo "Checking for broken links in documentation..."
|
||||||
|
npx --yes mintlify@latest broken-links
|
||||||
|
echo "✅ No broken links found"
|
||||||
73
.github/workflows/e2e-tests.yml
vendored
Normal file
73
.github/workflows/e2e-tests.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
name: E2E Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["02-27-feat_extend_e2e_ui_tests"]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: e2e-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-e2e-ui:
|
||||||
|
name: E2E UI (Playwright)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 45
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
|
with:
|
||||||
|
go-version: "1.26.2"
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version: "25"
|
||||||
|
|
||||||
|
- name: Set up Docker Compose
|
||||||
|
run: |
|
||||||
|
docker --version
|
||||||
|
if ! docker compose version >/dev/null 2>&1; then
|
||||||
|
echo "Installing Docker Compose plugin..."
|
||||||
|
DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
|
||||||
|
mkdir -p "$DOCKER_CONFIG/cli-plugins"
|
||||||
|
curl -SL "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o "$DOCKER_CONFIG/cli-plugins/docker-compose"
|
||||||
|
chmod +x "$DOCKER_CONFIG/cli-plugins/docker-compose"
|
||||||
|
docker compose version
|
||||||
|
else
|
||||||
|
echo "Docker Compose plugin is available"
|
||||||
|
docker compose version
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run E2E UI tests
|
||||||
|
env:
|
||||||
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
# Optional: for SSE MCP tests (e.g. remote proxy). Set in repo secrets.
|
||||||
|
MCP_SSE_HEADERS: ${{ secrets.MCP_SSE_HEADERS }}
|
||||||
|
run: ./.github/workflows/scripts/test-e2e-ui.sh
|
||||||
|
|
||||||
|
- name: Upload Playwright artifacts
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: |
|
||||||
|
tests/e2e/test-results/
|
||||||
|
tests/e2e/playwright-report/
|
||||||
|
retention-days: 7
|
||||||
129
.github/workflows/helm-release.yml
vendored
Normal file
129
.github/workflows/helm-release.yml
vendored
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
name: Release Helm Chart
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "helm-charts/bifrost/**"
|
||||||
|
- ".github/workflows/helm-release.yml"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: block
|
||||||
|
allowed-endpoints: >
|
||||||
|
api.github.com:443
|
||||||
|
get.helm.sh:443
|
||||||
|
github.com:443
|
||||||
|
maximhq.github.io:443
|
||||||
|
proxy.golang.org:443
|
||||||
|
release-assets.githubusercontent.com:443
|
||||||
|
storage.googleapis.com:443
|
||||||
|
sum.golang.org:443
|
||||||
|
uploads.github.com:443
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure Git
|
||||||
|
run: |
|
||||||
|
git config user.name "$GITHUB_ACTOR"
|
||||||
|
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Install Helm
|
||||||
|
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
|
||||||
|
with:
|
||||||
|
version: v4.0.0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
|
with:
|
||||||
|
go-version: "1.26.2"
|
||||||
|
|
||||||
|
- name: Run chart-testing (lint)
|
||||||
|
run: |
|
||||||
|
helm lint helm-charts/bifrost
|
||||||
|
|
||||||
|
- name: Validate Helm templates
|
||||||
|
run: |
|
||||||
|
chmod +x .github/workflows/scripts/validate-helm-templates.sh
|
||||||
|
.github/workflows/scripts/validate-helm-templates.sh
|
||||||
|
|
||||||
|
- name: Validate Helm config fields
|
||||||
|
run: |
|
||||||
|
chmod +x .github/workflows/scripts/validate-helm-config-fields.sh
|
||||||
|
.github/workflows/scripts/validate-helm-config-fields.sh
|
||||||
|
|
||||||
|
- name: Validate Go ↔ config.schema.json ↔ helm-chart sync (schemasync)
|
||||||
|
run: |
|
||||||
|
chmod +x .github/workflows/scripts/validate-schema-sync.sh
|
||||||
|
.github/workflows/scripts/validate-schema-sync.sh
|
||||||
|
|
||||||
|
- name: Get chart version
|
||||||
|
id: chart-version
|
||||||
|
run: |
|
||||||
|
VERSION=$(grep '^version:' helm-charts/bifrost/Chart.yaml | awk '{print $2}')
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Chart version: $VERSION"
|
||||||
|
|
||||||
|
- name: Check if release exists
|
||||||
|
id: check-release
|
||||||
|
run: |
|
||||||
|
if gh release view "helm-chart-v${{ steps.chart-version.outputs.version }}" &>/dev/null; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Package Helm chart
|
||||||
|
run: |
|
||||||
|
cd helm-charts
|
||||||
|
helm package bifrost
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
if: steps.check-release.outputs.exists == 'false'
|
||||||
|
run: |
|
||||||
|
cd helm-charts
|
||||||
|
gh release create "helm-chart-v${{ steps.chart-version.outputs.version }}" \
|
||||||
|
bifrost-${{ steps.chart-version.outputs.version }}.tgz \
|
||||||
|
--title "Helm Chart v${{ steps.chart-version.outputs.version }}" \
|
||||||
|
--notes "Helm chart release for Bifrost v${{ steps.chart-version.outputs.version }}"
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Update Helm repository index
|
||||||
|
run: |
|
||||||
|
cd helm-charts
|
||||||
|
# Download existing index if it exists
|
||||||
|
curl -sLO https://maximhq.github.io/bifrost/helm-charts/index.yaml || true
|
||||||
|
# Merge with new chart only if index.yaml exists
|
||||||
|
if [ -f index.yaml ]; then
|
||||||
|
helm repo index . --url https://github.com/maximhq/bifrost/releases/download/helm-chart-v${{ steps.chart-version.outputs.version }} --merge index.yaml
|
||||||
|
else
|
||||||
|
helm repo index . --url https://github.com/maximhq/bifrost/releases/download/helm-chart-v${{ steps.chart-version.outputs.version }}
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
|
||||||
|
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/v1.5.0'
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
publish_dir: ./helm-charts
|
||||||
|
destination_dir: helm-charts
|
||||||
|
keep_files: false
|
||||||
|
enable_jekyll: false
|
||||||
|
user_name: "github-actions[bot]"
|
||||||
|
user_email: "github-actions[bot]@users.noreply.github.com"
|
||||||
302
.github/workflows/npx-publish.yml
vendored
Normal file
302
.github/workflows/npx-publish.yml
vendored
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
name: NPX Package Publish
|
||||||
|
|
||||||
|
# Triggers when main is pushed and package.json has changed
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'npx/bifrost/package.json'
|
||||||
|
- 'npx/bifrost-cli/package.json'
|
||||||
|
|
||||||
|
# Prevent concurrent runs for the same trigger
|
||||||
|
concurrency:
|
||||||
|
group: npx-publish-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Check if pipeline should be skipped based on first line of commit message
|
||||||
|
check-skip:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
outputs:
|
||||||
|
should-skip: ${{ steps.check.outputs.should-skip }}
|
||||||
|
steps:
|
||||||
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Check if pipeline should be skipped
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
COMMIT_MESSAGE=$(git log -1 --pretty=%B)
|
||||||
|
FIRST_LINE=$(echo "$COMMIT_MESSAGE" | head -n 1)
|
||||||
|
if [[ "$FIRST_LINE" == *"--skip-ci"* ]]; then
|
||||||
|
echo "should-skip=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "should-skip=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
publish-bifrost:
|
||||||
|
needs: [check-skip]
|
||||||
|
if: needs.check-skip.outputs.should-skip != 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write # Required for npm provenance
|
||||||
|
steps:
|
||||||
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version: "25"
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: |
|
||||||
|
npx/bifrost/package-lock.json
|
||||||
|
|
||||||
|
- name: Check if bifrost package.json changed
|
||||||
|
id: check-bifrost
|
||||||
|
run: |
|
||||||
|
if git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" | grep -q '^npx/bifrost/package.json$'; then
|
||||||
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Extract version from package.json
|
||||||
|
if: steps.check-bifrost.outputs.changed == 'true'
|
||||||
|
id: extract-version
|
||||||
|
run: ./.github/workflows/scripts/extract-npx-version.sh
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.check-bifrost.outputs.changed == 'true'
|
||||||
|
working-directory: npx/bifrost
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
if: steps.check-bifrost.outputs.changed == 'true'
|
||||||
|
working-directory: npx/bifrost
|
||||||
|
run: |
|
||||||
|
if [ -f "package.json" ] && npm run | grep -q "test"; then
|
||||||
|
echo "Running tests..."
|
||||||
|
npm test
|
||||||
|
else
|
||||||
|
echo "No tests found, skipping..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
if: steps.check-bifrost.outputs.changed == 'true'
|
||||||
|
working-directory: npx/bifrost
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.extract-version.outputs.version }}"
|
||||||
|
echo "Publishing @maximhq/bifrost@${VERSION} to npm..."
|
||||||
|
if npm view @maximhq/bifrost@"${VERSION}" version >/dev/null 2>&1; then
|
||||||
|
echo "@maximhq/bifrost@${VERSION} already exists on npm. Skipping publish."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try OIDC (Trusted Publishing) first - no token needed
|
||||||
|
echo "Attempting publish with OIDC (Trusted Publishing)..."
|
||||||
|
if npm publish --provenance --access public 2>&1; then
|
||||||
|
echo "Published successfully with OIDC!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback to NPM_TOKEN if OIDC fails
|
||||||
|
if [ -n "$NPM_TOKEN" ]; then
|
||||||
|
echo "OIDC failed, falling back to NPM_TOKEN..."
|
||||||
|
export NODE_AUTH_TOKEN="$NPM_TOKEN"
|
||||||
|
npm publish --access public
|
||||||
|
else
|
||||||
|
echo "OIDC failed and no NPM_TOKEN available"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Configure Git
|
||||||
|
if: steps.check-bifrost.outputs.changed == 'true'
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
if: steps.check-bifrost.outputs.changed == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: bash .github/workflows/scripts/create-npx-release.sh "${{ steps.extract-version.outputs.version }}" "${{ steps.extract-version.outputs.full-tag }}"
|
||||||
|
|
||||||
|
- name: Discord Notification
|
||||||
|
if: always() && steps.check-bifrost.outputs.changed == 'true'
|
||||||
|
env:
|
||||||
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
run: |
|
||||||
|
AUTHOR="${{ github.actor }}"
|
||||||
|
COMMIT_AUTHOR="$(git log -1 --pretty=%an || true)"
|
||||||
|
if [ -n "$COMMIT_AUTHOR" ]; then AUTHOR="$COMMIT_AUTHOR"; fi
|
||||||
|
if [ "${{ job.status }}" = "success" ]; then
|
||||||
|
TITLE="**NPX Package Published**"
|
||||||
|
STATUS="Success"
|
||||||
|
VERSION_LINE="**Version**: \`${{ steps.extract-version.outputs.version }}\`"
|
||||||
|
PACKAGE_LINE="**Package**: \`@maximhq/bifrost\`"
|
||||||
|
NPM_LINK="**[View on npm](https://www.npmjs.com/package/@maximhq/bifrost)**"
|
||||||
|
MESSAGE="$TITLE\n**Status**: $STATUS\n$VERSION_LINE\n$PACKAGE_LINE\n$NPM_LINK\n**Tag**: \`${{ steps.extract-version.outputs.full-tag }}\`\n**Commit**: \`${{ github.sha }}\`\n**Author**: ${AUTHOR}\n**[View Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})**"
|
||||||
|
else
|
||||||
|
TITLE="**NPX Package Publish Failed**"
|
||||||
|
STATUS="Failed"
|
||||||
|
MESSAGE="$TITLE\n**Status**: $STATUS\n**Tag**: \`${{ steps.extract-version.outputs.full-tag }}\`\n**Commit**: \`${{ github.sha }}\`\n**Author**: ${AUTHOR}\n**[View Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})**"
|
||||||
|
fi
|
||||||
|
payload="$(jq -n --arg content "$MESSAGE" '{content:$content}')"
|
||||||
|
curl -sS -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK"
|
||||||
|
|
||||||
|
publish-bifrost-cli:
|
||||||
|
needs: [check-skip]
|
||||||
|
if: needs.check-skip.outputs.should-skip != 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write # Required for npm provenance
|
||||||
|
steps:
|
||||||
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version: "25"
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
|
- name: Check if bifrost-cli package.json changed
|
||||||
|
id: check-cli
|
||||||
|
run: |
|
||||||
|
if git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" | grep -q '^npx/bifrost-cli/package.json$'; then
|
||||||
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Extract version
|
||||||
|
if: steps.check-cli.outputs.changed == 'true'
|
||||||
|
id: extract-version
|
||||||
|
run: |
|
||||||
|
VERSION=$(jq -r '.version' npx/bifrost-cli/package.json)
|
||||||
|
if [[ -z "$VERSION" ]] || [[ "$VERSION" == "null" ]]; then
|
||||||
|
echo "Failed to extract version from npx/bifrost-cli/package.json"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "full-tag=npx/bifrost-cli/v${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Extracted bifrost-cli version: ${VERSION}"
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
if: steps.check-cli.outputs.changed == 'true'
|
||||||
|
working-directory: npx/bifrost-cli
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.extract-version.outputs.version }}"
|
||||||
|
echo "Publishing @maximhq/bifrost-cli@${VERSION} to npm..."
|
||||||
|
if npm view @maximhq/bifrost-cli@"${VERSION}" version >/dev/null 2>&1; then
|
||||||
|
echo "@maximhq/bifrost-cli@${VERSION} already exists on npm. Skipping publish."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try OIDC (Trusted Publishing) first - no token needed
|
||||||
|
echo "Attempting publish with OIDC (Trusted Publishing)..."
|
||||||
|
if npm publish --provenance --access public 2>&1; then
|
||||||
|
echo "Published successfully with OIDC!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback to NPM_TOKEN if OIDC fails
|
||||||
|
if [ -n "$NPM_TOKEN" ]; then
|
||||||
|
echo "OIDC failed, falling back to NPM_TOKEN..."
|
||||||
|
export NODE_AUTH_TOKEN="$NPM_TOKEN"
|
||||||
|
npm publish --access public
|
||||||
|
else
|
||||||
|
echo "OIDC failed and no NPM_TOKEN available"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Configure Git
|
||||||
|
if: steps.check-cli.outputs.changed == 'true'
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
if: steps.check-cli.outputs.changed == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.extract-version.outputs.version }}"
|
||||||
|
FULL_TAG="${{ steps.extract-version.outputs.full-tag }}"
|
||||||
|
PRERELEASE_FLAG=""
|
||||||
|
if [[ "$VERSION" == *-* ]]; then
|
||||||
|
PRERELEASE_FLAG="--prerelease"
|
||||||
|
fi
|
||||||
|
if gh release view "$FULL_TAG" >/dev/null 2>&1; then
|
||||||
|
echo "Release $FULL_TAG already exists. Skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if ! git rev-parse "$FULL_TAG" >/dev/null 2>&1; then
|
||||||
|
git tag "$FULL_TAG"
|
||||||
|
git push origin "$FULL_TAG"
|
||||||
|
fi
|
||||||
|
gh release create "$FULL_TAG" \
|
||||||
|
--title "Bifrost CLI v$VERSION" \
|
||||||
|
--notes "## Bifrost CLI v$VERSION
|
||||||
|
|
||||||
|
Install or run via npx:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
npx -y @maximhq/bifrost-cli
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
- [View on npm](https://www.npmjs.com/package/@maximhq/bifrost-cli)
|
||||||
|
- [Documentation](https://docs.getbifrost.ai/quickstart/cli/getting-started)" \
|
||||||
|
--latest=false \
|
||||||
|
${PRERELEASE_FLAG}
|
||||||
|
|
||||||
|
- name: Discord Notification
|
||||||
|
if: always() && steps.check-cli.outputs.changed == 'true'
|
||||||
|
env:
|
||||||
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
run: |
|
||||||
|
AUTHOR="${{ github.actor }}"
|
||||||
|
COMMIT_AUTHOR="$(git log -1 --pretty=%an || true)"
|
||||||
|
if [ -n "$COMMIT_AUTHOR" ]; then AUTHOR="$COMMIT_AUTHOR"; fi
|
||||||
|
if [ "${{ job.status }}" = "success" ]; then
|
||||||
|
MESSAGE="**NPX Bifrost CLI Published**\n**Status**: Success\n**Version**: \`${{ steps.extract-version.outputs.version }}\`\n**Package**: \`@maximhq/bifrost-cli\`\n**[View on npm](https://www.npmjs.com/package/@maximhq/bifrost-cli)**\n**Commit**: \`${{ github.sha }}\`\n**Author**: ${AUTHOR}\n**[View Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})**"
|
||||||
|
else
|
||||||
|
MESSAGE="**NPX Bifrost CLI Publish Failed**\n**Status**: Failed\n**Commit**: \`${{ github.sha }}\`\n**Author**: ${AUTHOR}\n**[View Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})**"
|
||||||
|
fi
|
||||||
|
payload="$(jq -n --arg content "$MESSAGE" '{content:$content}')"
|
||||||
|
curl -sS -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK"
|
||||||
65
.github/workflows/openapi-bundle.yml
vendored
Normal file
65
.github/workflows/openapi-bundle.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
name: OpenAPI Bundle
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/openapi-bundle.yml"
|
||||||
|
- "docs/openapi/**"
|
||||||
|
- "!docs/openapi/openapi.json"
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "docs/openapi/**"
|
||||||
|
- "!docs/openapi/openapi.json"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bundle-openapi:
|
||||||
|
name: Bundle OpenAPI Spec
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: block
|
||||||
|
allowed-endpoints: >
|
||||||
|
files.pythonhosted.org:443
|
||||||
|
github.com:443
|
||||||
|
pypi.org:443
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
- name: Configure Git
|
||||||
|
run: |
|
||||||
|
git config user.name "GitHub Actions Bot"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install pyyaml
|
||||||
|
|
||||||
|
- name: Bundle OpenAPI spec
|
||||||
|
working-directory: ./docs/openapi
|
||||||
|
run: python bundle.py
|
||||||
|
|
||||||
|
- name: Commit and push changes
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
run: |
|
||||||
|
CURRENT_BRANCH="${GITHUB_REF_NAME:-main}"
|
||||||
|
|
||||||
|
git add docs/openapi/openapi.json
|
||||||
|
if git diff --staged --quiet; then
|
||||||
|
echo "No changes to commit"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git commit -m "chore: regenerate openapi.json --skip-ci"
|
||||||
|
git push origin "$CURRENT_BRANCH"
|
||||||
59
.github/workflows/pr-test-notifier.yml
vendored
Normal file
59
.github/workflows/pr-test-notifier.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: PR Test Notifier
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Check if pipeline should be skipped based on first line of commit message
|
||||||
|
check-skip:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
should-skip: ${{ steps.check.outputs.should-skip }}
|
||||||
|
steps:
|
||||||
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Check if pipeline should be skipped
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
COMMIT_MESSAGE=$(git log -1 --pretty=%B)
|
||||||
|
FIRST_LINE=$(echo "$COMMIT_MESSAGE" | head -n 1)
|
||||||
|
if [[ "$FIRST_LINE" == *"--skip-ci"* ]]; then
|
||||||
|
echo "should-skip=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "should-skip=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
notify:
|
||||||
|
needs: [check-skip]
|
||||||
|
if: needs.check-skip.outputs.should-skip != 'true'
|
||||||
|
name: Post Test Instructions
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Post comment with test trigger instructions
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
gh pr comment ${{ github.event.pull_request.number }} \
|
||||||
|
--repo ${{ github.repository }} \
|
||||||
|
--body "## 🧪 Test Suite Available
|
||||||
|
|
||||||
|
This PR can be tested by a repository admin.
|
||||||
|
|
||||||
|
[Run tests for PR #${{ github.event.pull_request.number }}](https://github.com/${{ github.repository }}/actions/workflows/pr-tests.yml)"
|
||||||
163
.github/workflows/pr-tests.yml
vendored
Normal file
163
.github/workflows/pr-tests.yml
vendored
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
name: PR Tests (Requires Approval)
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Manual trigger only - requires admin to click "Run workflow" button
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
pr_number:
|
||||||
|
description: "PR number to test (leave empty for current branch)"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
# Prevent concurrent test runs on the same PR
|
||||||
|
concurrency:
|
||||||
|
group: pr-tests-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Check if pipeline should be skipped based on first line of commit message
|
||||||
|
check-skip:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
outputs:
|
||||||
|
should-skip: ${{ steps.check.outputs.should-skip }}
|
||||||
|
steps:
|
||||||
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Check if pipeline should be skipped
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
COMMIT_MESSAGE=$(git log -1 --pretty=%B)
|
||||||
|
FIRST_LINE=$(echo "$COMMIT_MESSAGE" | head -n 1)
|
||||||
|
if [[ "$FIRST_LINE" == *"--skip-ci"* ]]; then
|
||||||
|
echo "should-skip=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "should-skip=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# This job shows up immediately and waits for approval
|
||||||
|
run-tests:
|
||||||
|
needs: [check-skip]
|
||||||
|
if: needs.check-skip.outputs.should-skip != 'true'
|
||||||
|
name: Run Tests (Awaiting Approval)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# Environment with protection rules - requires admin approval
|
||||||
|
# Note: You need to configure this environment in repo settings
|
||||||
|
environment:
|
||||||
|
name: pr-testing
|
||||||
|
url: ${{ github.event.pull_request.html_url || github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
|
with:
|
||||||
|
go-version: "1.26.2"
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version: "25"
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
- name: Add comment to PR
|
||||||
|
if: github.event.pull_request.number
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
gh pr comment ${{ github.event.pull_request.number }} --body "🧪 Test run approved and starting...
|
||||||
|
|
||||||
|
**Test Suite Includes:**
|
||||||
|
- 📦 Core Build Validation
|
||||||
|
- 🔌 MCP Test Servers Build
|
||||||
|
- 🔧 Core Provider Tests
|
||||||
|
- 🛡️ Governance Tests
|
||||||
|
- 🔗 Integration Tests
|
||||||
|
|
||||||
|
[View workflow run →](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
|
||||||
|
|
||||||
|
- name: Make test script executable
|
||||||
|
run: chmod +x .github/workflows/scripts/run-tests.sh
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
env:
|
||||||
|
# API Keys for provider tests
|
||||||
|
MAXIM_API_KEY: ${{ secrets.MAXIM_API_KEY }}
|
||||||
|
MAXIM_LOGGER_ID: ${{ secrets.MAXIM_LOG_REPO_ID }}
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_SESSION_TOKEN: ${{ secrets.AWS_SESSION_TOKEN }}
|
||||||
|
AWS_ARN: ${{ secrets.AWS_ARN }}
|
||||||
|
BEDROCK_API_KEY: ${{ secrets.BEDROCK_API_KEY }}
|
||||||
|
AZURE_ENDPOINT: ${{ secrets.AZURE_ENDPOINT }}
|
||||||
|
AZURE_API_KEY: ${{ secrets.AZURE_API_KEY }}
|
||||||
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||||
|
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||||
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||||
|
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||||
|
PARASAIL_API_KEY: ${{ secrets.PARASAIL_API_KEY }}
|
||||||
|
PERPLEXITY_API_KEY: ${{ secrets.PERPLEXITY_API_KEY }}
|
||||||
|
ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }}
|
||||||
|
SGL_API_KEY: ${{ secrets.SGL_API_KEY }}
|
||||||
|
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
|
||||||
|
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
|
||||||
|
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||||
|
VERTEX_CREDENTIALS: ${{ secrets.VERTEX_CREDENTIALS }}
|
||||||
|
VERTEX_PROJECT_ID: ${{ secrets.VERTEX_PROJECT_ID }}
|
||||||
|
HUGGING_FACE_API_KEY: ${{ secrets.HUGGING_FACE_API_KEY }}
|
||||||
|
REPLICATE_API_KEY: ${{ secrets.REPLICATE_API_KEY }}
|
||||||
|
REPLICATE_OWNER : ${{ secrets.REPLICATE_OWNER }}
|
||||||
|
RUNWAY_API_KEY : ${{ secrets.RUNWAY_API_KEY }}
|
||||||
|
run: |
|
||||||
|
echo "Running tests for PR #${{ github.event.pull_request.number || 'manual run' }}"
|
||||||
|
./.github/workflows/scripts/run-tests.sh
|
||||||
|
|
||||||
|
- name: Report test results
|
||||||
|
if: always() && github.event.pull_request.number
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
if [ "${{ job.status }}" = "success" ]; then
|
||||||
|
gh pr comment ${{ github.event.pull_request.number }} --body "✅ **All tests passed successfully!**
|
||||||
|
|
||||||
|
All test suites have completed without errors. This PR is ready for review.
|
||||||
|
|
||||||
|
[View detailed results →](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
|
||||||
|
else
|
||||||
|
gh pr comment ${{ github.event.pull_request.number }} --body "❌ **Tests failed**
|
||||||
|
|
||||||
|
One or more test suites failed. Please review the failures and update your PR.
|
||||||
|
|
||||||
|
[View detailed results →](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
|
||||||
|
fi
|
||||||
138
.github/workflows/release-cli.yml
vendored
Normal file
138
.github/workflows/release-cli.yml
vendored
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
name: Release CLI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
# Prevent concurrent runs
|
||||||
|
concurrency:
|
||||||
|
group: release-cli
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-version:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.get-version.outputs.version }}
|
||||||
|
tag_exists: ${{ steps.check-tag.outputs.exists }}
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: block
|
||||||
|
allowed-endpoints: >
|
||||||
|
github.com:443
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Get version from file
|
||||||
|
id: get-version
|
||||||
|
run: echo "version=$(cat cli/version)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Check if tag exists
|
||||||
|
id: check-tag
|
||||||
|
run: |
|
||||||
|
if git rev-parse "cli/v${{ steps.get-version.outputs.version }}" >/dev/null 2>&1; then
|
||||||
|
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
test-cli:
|
||||||
|
needs: [check-version]
|
||||||
|
if: needs.check-version.outputs.tag_exists == 'false'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
|
with:
|
||||||
|
go-version: "1.26.2"
|
||||||
|
|
||||||
|
- name: Run CLI tests
|
||||||
|
working-directory: cli
|
||||||
|
run: go test ./...
|
||||||
|
|
||||||
|
release-cli:
|
||||||
|
needs: [check-version, test-cli]
|
||||||
|
if: needs.check-version.outputs.tag_exists == 'false'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
outputs:
|
||||||
|
success: ${{ steps.release.outputs.success }}
|
||||||
|
steps:
|
||||||
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
token: ${{ secrets.GH_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
|
with:
|
||||||
|
go-version: "1.26.2"
|
||||||
|
|
||||||
|
- name: Configure Git
|
||||||
|
run: |
|
||||||
|
git config user.name "GitHub Actions Bot"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Release CLI
|
||||||
|
id: release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||||
|
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||||
|
run: ./.github/workflows/scripts/release-cli.sh "${{ needs.check-version.outputs.version }}"
|
||||||
|
|
||||||
|
push-mintlify-changelog:
|
||||||
|
needs: [check-version, release-cli]
|
||||||
|
if: needs.check-version.outputs.tag_exists == 'false' && needs.release-cli.result == 'success'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
token: ${{ secrets.GH_TOKEN }}
|
||||||
|
|
||||||
|
- name: Push Mintlify changelog
|
||||||
|
run: |
|
||||||
|
./.github/workflows/scripts/push-cli-mintlify-changelog.sh "${{ needs.check-version.outputs.version }}"
|
||||||
1822
.github/workflows/release-pipeline.yml
vendored
Normal file
1822
.github/workflows/release-pipeline.yml
vendored
Normal file
File diff suppressed because it is too large
Load Diff
94
.github/workflows/scorecards.yml
vendored
Normal file
94
.github/workflows/scorecards.yml
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# This workflow uses actions that are not certified by GitHub. They are provided
|
||||||
|
# by a third-party and are governed by separate terms of service, privacy
|
||||||
|
# policy, and support documentation.
|
||||||
|
|
||||||
|
name: Scorecard supply-chain security
|
||||||
|
on:
|
||||||
|
# For Branch-Protection check. Only the default branch is supported. See
|
||||||
|
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
|
||||||
|
branch_protection_rule:
|
||||||
|
# To guarantee Maintained check is occasionally updated. See
|
||||||
|
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
|
||||||
|
schedule:
|
||||||
|
- cron: '20 7 * * 2'
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
# Declare default permissions as read only.
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analysis:
|
||||||
|
name: Scorecard analysis
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
# Needed to upload the results to code-scanning dashboard.
|
||||||
|
security-events: write
|
||||||
|
# Needed to publish results and get a badge (see publish_results below).
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
# To allow GraphQL ListCommits to work
|
||||||
|
issues: read
|
||||||
|
pull-requests: read
|
||||||
|
# To detect SAST tools
|
||||||
|
checks: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: block
|
||||||
|
allowed-endpoints: >
|
||||||
|
api.deps.dev:443
|
||||||
|
api.github.com:443
|
||||||
|
api.osv.dev:443
|
||||||
|
api.scorecard.dev:443
|
||||||
|
auth.docker.io:443
|
||||||
|
fulcio.sigstore.dev:443
|
||||||
|
github.com:443
|
||||||
|
index.docker.io:443
|
||||||
|
oss-fuzz-build-logs.storage.googleapis.com:443
|
||||||
|
rekor.sigstore.dev:443
|
||||||
|
tuf-repo-cdn.sigstore.dev:443
|
||||||
|
www.bestpractices.dev:443
|
||||||
|
|
||||||
|
- name: "Checkout code"
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: "Run analysis"
|
||||||
|
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
|
||||||
|
with:
|
||||||
|
results_file: results.sarif
|
||||||
|
results_format: sarif
|
||||||
|
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
|
||||||
|
# - you want to enable the Branch-Protection check on a *public* repository, or
|
||||||
|
# - you are installing Scorecards on a *private* repository
|
||||||
|
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
|
||||||
|
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
|
||||||
|
|
||||||
|
# Public repositories:
|
||||||
|
# - Publish results to OpenSSF REST API for easy access by consumers
|
||||||
|
# - Allows the repository to include the Scorecard badge.
|
||||||
|
# - See https://github.com/ossf/scorecard-action#publishing-results.
|
||||||
|
# For private repositories:
|
||||||
|
# - `publish_results` will always be set to `false`, regardless
|
||||||
|
# of the value entered here.
|
||||||
|
publish_results: true
|
||||||
|
|
||||||
|
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||||
|
# format to the repository Actions tab.
|
||||||
|
- name: "Upload artifact"
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
|
with:
|
||||||
|
name: SARIF file
|
||||||
|
path: results.sarif
|
||||||
|
retention-days: 5
|
||||||
|
|
||||||
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
|
- name: "Upload to code-scanning"
|
||||||
|
uses: github/codeql-action/upload-sarif@5c8a8a642e79153f5d047b10ec1cba1d1cc65699 # v3.35.1
|
||||||
|
with:
|
||||||
|
sarif_file: results.sarif
|
||||||
59
.github/workflows/scripts/build-cli-executables.sh
vendored
Executable file
59
.github/workflows/scripts/build-cli-executables.sh
vendored
Executable file
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Cross-compile CLI binaries for multiple platforms
|
||||||
|
# Usage: ./build-cli-executables.sh <version>
|
||||||
|
|
||||||
|
if [[ -z "${1:-}" ]]; then
|
||||||
|
echo "Usage: $0 <version>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
VERSION="$1"
|
||||||
|
|
||||||
|
echo "🔨 Building CLI executables with version: $VERSION"
|
||||||
|
|
||||||
|
# Get the script directory and project root
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||||
|
|
||||||
|
# Clean and create dist directory
|
||||||
|
rm -rf "$PROJECT_ROOT/dist"
|
||||||
|
mkdir -p "$PROJECT_ROOT/dist"
|
||||||
|
|
||||||
|
# Define platforms
|
||||||
|
platforms=(
|
||||||
|
"darwin/amd64"
|
||||||
|
"darwin/arm64"
|
||||||
|
"linux/amd64"
|
||||||
|
"linux/arm64"
|
||||||
|
"windows/amd64"
|
||||||
|
)
|
||||||
|
|
||||||
|
MODULE_PATH="$PROJECT_ROOT/cli"
|
||||||
|
COMMIT="${GITHUB_SHA:-$(git rev-parse HEAD 2>/dev/null || echo 'unknown')}"
|
||||||
|
|
||||||
|
for platform in "${platforms[@]}"; do
|
||||||
|
IFS='/' read -r GOOS GOARCH <<< "$platform"
|
||||||
|
|
||||||
|
output_name="bifrost"
|
||||||
|
[[ "$GOOS" = "windows" ]] && output_name+='.exe'
|
||||||
|
|
||||||
|
echo "Building bifrost CLI for $GOOS/$GOARCH..."
|
||||||
|
mkdir -p "$PROJECT_ROOT/dist/$GOOS/$GOARCH"
|
||||||
|
|
||||||
|
cd "$MODULE_PATH"
|
||||||
|
|
||||||
|
# CLI has no CGO dependencies, so we can cross-compile without cross-compilers
|
||||||
|
env GOWORK=off CGO_ENABLED=0 GOOS="$GOOS" GOARCH="$GOARCH" \
|
||||||
|
go build -trimpath \
|
||||||
|
-ldflags "-s -w -buildid= -X main.version=v${VERSION} -X main.commit=${COMMIT}" \
|
||||||
|
-o "$PROJECT_ROOT/dist/$GOOS/$GOARCH/$output_name" .
|
||||||
|
|
||||||
|
# Generate SHA-256 checksum for the binary
|
||||||
|
(cd "$PROJECT_ROOT/dist/$GOOS/$GOARCH" && shasum -a 256 "$output_name" > "$output_name.sha256")
|
||||||
|
echo " → checksum: $(cat "$PROJECT_ROOT/dist/$GOOS/$GOARCH/$output_name.sha256")"
|
||||||
|
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "✅ All CLI binaries built successfully"
|
||||||
125
.github/workflows/scripts/build-executables.sh
vendored
Executable file
125
.github/workflows/scripts/build-executables.sh
vendored
Executable file
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Cross-compile Go binaries for multiple platforms
|
||||||
|
# Usage: ./build-executables.sh <version> [platforms]
|
||||||
|
# Examples:
|
||||||
|
# ./build-executables.sh 1.4.15 # Build all platforms
|
||||||
|
# ./build-executables.sh 1.4.15 "darwin/amd64 darwin/arm64 linux/amd64 windows/amd64" # Build specific platforms
|
||||||
|
# ./build-executables.sh 1.4.15 "linux/arm64" # Build single platform (native on ARM)
|
||||||
|
|
||||||
|
# Require version argument (matches usage)
|
||||||
|
if [[ -z "${1:-}" ]]; then
|
||||||
|
echo "Usage: $0 <version> [platforms]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
VERSION="$1"
|
||||||
|
PLATFORM_FILTER="${2:-}"
|
||||||
|
|
||||||
|
echo "🔨 Building Go executables with version: $VERSION"
|
||||||
|
|
||||||
|
# Get the script directory and project root
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||||
|
|
||||||
|
# Clean and create dist directory
|
||||||
|
rm -rf "$PROJECT_ROOT/dist"
|
||||||
|
mkdir -p "$PROJECT_ROOT/dist"
|
||||||
|
|
||||||
|
# Define platforms — use filter if provided, otherwise build all
|
||||||
|
all_platforms=(
|
||||||
|
"darwin/amd64"
|
||||||
|
"darwin/arm64"
|
||||||
|
"linux/amd64"
|
||||||
|
"linux/arm64"
|
||||||
|
"windows/amd64"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ -n "$PLATFORM_FILTER" ]]; then
|
||||||
|
platforms=()
|
||||||
|
for p in $PLATFORM_FILTER; do
|
||||||
|
platforms+=("$p")
|
||||||
|
done
|
||||||
|
echo "📋 Building filtered platforms: ${platforms[*]}"
|
||||||
|
else
|
||||||
|
platforms=("${all_platforms[@]}")
|
||||||
|
echo "📋 Building all platforms: ${platforms[*]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect host architecture for native build detection
|
||||||
|
HOST_ARCH=$(uname -m)
|
||||||
|
|
||||||
|
MODULE_PATH="$PROJECT_ROOT/transports/bifrost-http"
|
||||||
|
|
||||||
|
|
||||||
|
for platform in "${platforms[@]}"; do
|
||||||
|
IFS='/' read -r PLATFORM_DIR GOARCH <<< "$platform"
|
||||||
|
|
||||||
|
case "$PLATFORM_DIR" in
|
||||||
|
"windows") GOOS="windows" ;;
|
||||||
|
"darwin") GOOS="darwin" ;;
|
||||||
|
"linux") GOOS="linux" ;;
|
||||||
|
*) echo "Unsupported platform: $PLATFORM_DIR"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
output_name="bifrost-http"
|
||||||
|
[[ "$GOOS" = "windows" ]] && output_name+='.exe'
|
||||||
|
|
||||||
|
echo "Building bifrost-http for $PLATFORM_DIR/$GOARCH..."
|
||||||
|
mkdir -p "$PROJECT_ROOT/dist/$PLATFORM_DIR/$GOARCH"
|
||||||
|
|
||||||
|
# Change to the module directory for building
|
||||||
|
cd "$MODULE_PATH"
|
||||||
|
|
||||||
|
if [[ "$GOOS" = "linux" ]]; then
|
||||||
|
# Detect native build: if target arch matches host, use system compiler
|
||||||
|
if [[ "$GOARCH" = "arm64" ]] && [[ "$HOST_ARCH" = "aarch64" || "$HOST_ARCH" = "arm64" ]]; then
|
||||||
|
echo " 🏠 Native ARM64 build detected — using system compiler"
|
||||||
|
CC_COMPILER="${CC:-gcc}"
|
||||||
|
CXX_COMPILER="${CXX:-g++}"
|
||||||
|
elif [[ "$GOARCH" = "amd64" ]] && [[ "$HOST_ARCH" = "x86_64" ]]; then
|
||||||
|
echo " 🏠 Native AMD64 build detected — using system compiler"
|
||||||
|
CC_COMPILER="${CC:-gcc}"
|
||||||
|
CXX_COMPILER="${CXX:-g++}"
|
||||||
|
elif [[ "$GOARCH" = "amd64" ]]; then
|
||||||
|
CC_COMPILER="x86_64-linux-musl-gcc"
|
||||||
|
CXX_COMPILER="x86_64-linux-musl-g++"
|
||||||
|
elif [[ "$GOARCH" = "arm64" ]]; then
|
||||||
|
CC_COMPILER="aarch64-linux-musl-gcc"
|
||||||
|
CXX_COMPILER="aarch64-linux-musl-g++"
|
||||||
|
fi
|
||||||
|
|
||||||
|
env GOWORK=off CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" CC="$CC_COMPILER" CXX="$CXX_COMPILER" \
|
||||||
|
go build -trimpath -tags "netgo,osusergo,sqlite_static" \
|
||||||
|
-ldflags "-s -w -buildid= -extldflags '-static' -X main.Version=v${VERSION}" \
|
||||||
|
-o "$PROJECT_ROOT/dist/$PLATFORM_DIR/$GOARCH/$output_name" .
|
||||||
|
|
||||||
|
elif [[ "$GOOS" = "windows" ]]; then
|
||||||
|
if [[ "$GOARCH" = "amd64" ]]; then
|
||||||
|
CC_COMPILER="x86_64-w64-mingw32-gcc"
|
||||||
|
CXX_COMPILER="x86_64-w64-mingw32-g++"
|
||||||
|
fi
|
||||||
|
|
||||||
|
env GOWORK=off CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" CC="$CC_COMPILER" CXX="$CXX_COMPILER" \
|
||||||
|
go build -trimpath -ldflags "-s -w -buildid= -X main.Version=v${VERSION}" \
|
||||||
|
-o "$PROJECT_ROOT/dist/$PLATFORM_DIR/$GOARCH/$output_name" .
|
||||||
|
|
||||||
|
else # Darwin (macOS)
|
||||||
|
if [[ "$GOARCH" = "amd64" ]]; then
|
||||||
|
CC_COMPILER="o64-clang"
|
||||||
|
CXX_COMPILER="o64-clang++"
|
||||||
|
elif [[ "$GOARCH" = "arm64" ]]; then
|
||||||
|
CC_COMPILER="oa64-clang"
|
||||||
|
CXX_COMPILER="oa64-clang++"
|
||||||
|
fi
|
||||||
|
|
||||||
|
env GOWORK=off CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" CC="$CC_COMPILER" CXX="$CXX_COMPILER" \
|
||||||
|
go build -trimpath -ldflags "-s -w -buildid= -X main.Version=v${VERSION}" \
|
||||||
|
-o "$PROJECT_ROOT/dist/$PLATFORM_DIR/$GOARCH/$output_name" .
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Change back to project root
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "✅ All binaries built successfully"
|
||||||
19
.github/workflows/scripts/changelog-utils.sh
vendored
Normal file
19
.github/workflows/scripts/changelog-utils.sh
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Function to extract content from a file
|
||||||
|
# Usage: get_file_content <file_path>
|
||||||
|
# Returns the file content with comments removed, or empty string if file doesn't exist
|
||||||
|
get_file_content() {
|
||||||
|
if [ -f "$1" ]; then
|
||||||
|
content=$(cat "$1")
|
||||||
|
# Skip comments from content
|
||||||
|
content=$(echo "$content" | grep -v '^<!--' | grep -v '^-->')
|
||||||
|
# For version files, also trim newlines and whitespace
|
||||||
|
if [[ "$1" == *"/version" ]]; then
|
||||||
|
content=$(echo "$content" | tr -d '\n' | xargs)
|
||||||
|
fi
|
||||||
|
echo "$content"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
81
.github/workflows/scripts/check-dependency-flow.sh
vendored
Executable file
81
.github/workflows/scripts/check-dependency-flow.sh
vendored
Executable file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Check the dependency flow and suggest next steps
|
||||||
|
# Usage: ./check-dependency-flow.sh <stage> [version]
|
||||||
|
# stage: core|framework|plugins
|
||||||
|
# version: required for core/framework; optional for plugins
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 <stage: core|framework|plugins> [version]" >&2
|
||||||
|
echo "Examples:" >&2
|
||||||
|
echo " $0 core v1.2.3" >&2
|
||||||
|
echo " $0 framework v1.2.3" >&2
|
||||||
|
echo " $0 plugins" >&2
|
||||||
|
}
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
usage
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
STAGE="${1:-}"
|
||||||
|
VERSION="${2:-}"
|
||||||
|
|
||||||
|
# Validate stage first, then enforce version requirement by stage
|
||||||
|
case "$STAGE" in
|
||||||
|
core|framework|plugins)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "❌ Unknown stage: $STAGE" >&2
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# VERSION is required for core/framework; optional for plugins
|
||||||
|
if [[ "$STAGE" != "plugins" && -z "${VERSION:-}" ]]; then
|
||||||
|
echo "❌ VERSION is required for stage '$STAGE'." >&2
|
||||||
|
usage
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$STAGE" in
|
||||||
|
"core")
|
||||||
|
echo "🔧 Core v$VERSION released!"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Dependency Flow Status:"
|
||||||
|
echo "✅ Core: v$VERSION (just released)"
|
||||||
|
echo "❓ Framework: Check if update needed"
|
||||||
|
echo "❓ Plugins: Will check after framework"
|
||||||
|
echo "❓ Bifrost HTTP: Will check after plugins"
|
||||||
|
echo ""
|
||||||
|
echo "🔄 Next Step: Manually trigger Framework Release if needed"
|
||||||
|
;;
|
||||||
|
|
||||||
|
"framework")
|
||||||
|
echo "📦 Framework v$VERSION released!"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Dependency Flow Status:"
|
||||||
|
echo "✅ Core: (already updated)"
|
||||||
|
echo "✅ Framework: v$VERSION (just released)"
|
||||||
|
echo "❓ Plugins: Check if any need updates"
|
||||||
|
echo "❓ Bifrost HTTP: Will check after plugins"
|
||||||
|
echo ""
|
||||||
|
echo "🔄 Next Step: Check Plugins Release workflow"
|
||||||
|
;;
|
||||||
|
|
||||||
|
"plugins")
|
||||||
|
echo "🔌 Plugins ${VERSION:+v$VERSION }released!"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Dependency Flow Status:"
|
||||||
|
echo "✅ Core: (already updated)"
|
||||||
|
echo "✅ Framework: (already updated)"
|
||||||
|
echo "✅ Plugins: (just released)"
|
||||||
|
echo "❓ Bifrost HTTP: Check if update needed"
|
||||||
|
echo ""
|
||||||
|
echo "🔄 Next Step: Manually trigger Bifrost HTTP Release if needed"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "❌ Unknown stage: $STAGE"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
31
.github/workflows/scripts/configure-r2.sh
vendored
Executable file
31
.github/workflows/scripts/configure-r2.sh
vendored
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Configure AWS CLI for R2 uploads
|
||||||
|
# Usage: ./configure-r2.sh
|
||||||
|
|
||||||
|
echo "⚙️ Configuring AWS CLI for R2..."
|
||||||
|
|
||||||
|
pip install awscli
|
||||||
|
|
||||||
|
# Clean and trim environment variables (removing any whitespace)
|
||||||
|
R2_ENDPOINT="$(echo "$R2_ENDPOINT" | tr -d '[:space:]')"
|
||||||
|
R2_ACCESS_KEY_ID="$(echo "$R2_ACCESS_KEY_ID" | tr -d '[:space:]')"
|
||||||
|
R2_SECRET_ACCESS_KEY="$(echo "$R2_SECRET_ACCESS_KEY" | tr -d '[:space:]')"
|
||||||
|
|
||||||
|
# Validate environment variables
|
||||||
|
if [ -z "$R2_ENDPOINT" ] || [ -z "$R2_ACCESS_KEY_ID" ] || [ -z "$R2_SECRET_ACCESS_KEY" ]; then
|
||||||
|
echo "❌ Missing required R2 credentials"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configure AWS CLI for R2 using dedicated profile
|
||||||
|
aws configure set --profile R2 aws_access_key_id "$R2_ACCESS_KEY_ID"
|
||||||
|
aws configure set --profile R2 aws_secret_access_key "$R2_SECRET_ACCESS_KEY"
|
||||||
|
aws configure set --profile R2 region us-east-1
|
||||||
|
aws configure set --profile R2 s3.signature_version s3v4
|
||||||
|
|
||||||
|
# Test connection
|
||||||
|
echo "🔍 Testing R2 connection..."
|
||||||
|
aws s3 ls s3://prod-downloads/ --endpoint-url "$R2_ENDPOINT" --profile R2 >/dev/null
|
||||||
|
echo "✅ R2 connection successful"
|
||||||
36
.github/workflows/scripts/create-docker-manifest.sh
vendored
Executable file
36
.github/workflows/scripts/create-docker-manifest.sh
vendored
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
# Validate input argument
|
||||||
|
if [ "${1:-}" = "" ]; then
|
||||||
|
echo "Usage: $0 <version>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="$1"
|
||||||
|
REGISTRY="docker.io"
|
||||||
|
ACCOUNT="maximhq"
|
||||||
|
IMAGE_NAME="bifrost"
|
||||||
|
IMAGE="${REGISTRY}/${ACCOUNT}/${IMAGE_NAME}"
|
||||||
|
|
||||||
|
# Get the actual image digests from the platform-specific builds
|
||||||
|
AMD64_DIGEST=$(docker manifest inspect ${IMAGE}:v${VERSION}-amd64 | jq -r '.manifests[0].digest')
|
||||||
|
ARM64_DIGEST=$(docker manifest inspect ${IMAGE}:v${VERSION}-arm64 | jq -r '.manifests[0].digest')
|
||||||
|
|
||||||
|
echo "AMD64 digest: ${AMD64_DIGEST}"
|
||||||
|
echo "ARM64 digest: ${ARM64_DIGEST}"
|
||||||
|
|
||||||
|
# Create manifest for versioned tag using digests
|
||||||
|
docker manifest create \
|
||||||
|
${IMAGE}:v${VERSION} \
|
||||||
|
${IMAGE}@${AMD64_DIGEST} \
|
||||||
|
${IMAGE}@${ARM64_DIGEST}
|
||||||
|
|
||||||
|
docker manifest push ${IMAGE}:v${VERSION}
|
||||||
|
|
||||||
|
# Create latest manifest only for stable versions
|
||||||
|
if [[ "$VERSION" != *-* ]]; then
|
||||||
|
docker manifest create \
|
||||||
|
${IMAGE}:latest \
|
||||||
|
${IMAGE}@${AMD64_DIGEST} \
|
||||||
|
${IMAGE}@${ARM64_DIGEST}
|
||||||
|
|
||||||
|
docker manifest push ${IMAGE}:latest
|
||||||
|
fi
|
||||||
92
.github/workflows/scripts/create-npx-release.sh
vendored
Executable file
92
.github/workflows/scripts/create-npx-release.sh
vendored
Executable file
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Create GitHub release for NPX package
|
||||||
|
# Usage: ./create-npx-release.sh <version> <full-tag>
|
||||||
|
|
||||||
|
VERSION="$1"
|
||||||
|
FULL_TAG="$2"
|
||||||
|
|
||||||
|
if [[ -z "$VERSION" || -z "$FULL_TAG" ]]; then
|
||||||
|
echo "❌ Usage: $0 <version> <full-tag>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Mark prereleases when version contains a hyphen
|
||||||
|
PRERELEASE_FLAG=""
|
||||||
|
if [[ "$VERSION" == *-* ]]; then
|
||||||
|
PRERELEASE_FLAG="--prerelease"
|
||||||
|
fi
|
||||||
|
TITLE="NPX Package v$VERSION"
|
||||||
|
|
||||||
|
# Create release body
|
||||||
|
BODY="## NPX Package Release
|
||||||
|
|
||||||
|
### 📦 NPX Package v$VERSION
|
||||||
|
|
||||||
|
The Bifrost CLI is now available on npm!
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Install globally
|
||||||
|
npm install -g @maximhq/bifrost
|
||||||
|
|
||||||
|
# Or use with npx (no installation needed)
|
||||||
|
npx @maximhq/bifrost --help
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Start Bifrost HTTP server
|
||||||
|
bifrost
|
||||||
|
|
||||||
|
# Use specific transport version
|
||||||
|
bifrost --transport-version v1.2.3
|
||||||
|
|
||||||
|
# Get help
|
||||||
|
bifrost --help
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Links
|
||||||
|
|
||||||
|
- 📦 [View on npm](https://www.npmjs.com/package/@maximhq/bifrost)
|
||||||
|
- 📚 [Documentation](https://github.com/maximhq/bifrost)
|
||||||
|
- 🐛 [Report Issues](https://github.com/maximhq/bifrost/issues)
|
||||||
|
|
||||||
|
### What's New
|
||||||
|
|
||||||
|
This NPX package provides a convenient way to run Bifrost without manual binary downloads. The CLI automatically:
|
||||||
|
|
||||||
|
- Detects your platform and architecture
|
||||||
|
- Downloads the appropriate binary
|
||||||
|
- Supports version pinning with \`--transport-version\`
|
||||||
|
- Provides progress indicators for downloads
|
||||||
|
|
||||||
|
---
|
||||||
|
_This release was automatically created from tag \`$FULL_TAG\`_"
|
||||||
|
|
||||||
|
# Check if release already exists
|
||||||
|
echo "🔍 Checking if release $FULL_TAG already exists..."
|
||||||
|
if gh release view "$FULL_TAG" >/dev/null 2>&1; then
|
||||||
|
echo "ℹ️ Release $FULL_TAG already exists. Skipping creation."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if tag already exists
|
||||||
|
echo "🔍 Checking if tag $FULL_TAG exists..."
|
||||||
|
if git rev-parse "$FULL_TAG" >/dev/null 2>&1; then
|
||||||
|
echo "✅ Tag $FULL_TAG already exists."
|
||||||
|
else
|
||||||
|
echo "🏷️ Creating tag $FULL_TAG..."
|
||||||
|
git tag "$FULL_TAG"
|
||||||
|
git push origin "$FULL_TAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create release
|
||||||
|
echo "🎉 Creating GitHub release for $TITLE..."
|
||||||
|
gh release create "$FULL_TAG" \
|
||||||
|
--title "$TITLE" \
|
||||||
|
--notes "$BODY" \
|
||||||
|
--latest=false \
|
||||||
|
${PRERELEASE_FLAG}
|
||||||
382
.github/workflows/scripts/detect-all-changes.sh
vendored
Executable file
382
.github/workflows/scripts/detect-all-changes.sh
vendored
Executable file
@@ -0,0 +1,382 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
shopt -s nullglob
|
||||||
|
|
||||||
|
# Detect what components need to be released based on version changes
|
||||||
|
# Usage: ./detect-all-changes.sh
|
||||||
|
echo "🔍 Auto-detecting version changes across all components..."
|
||||||
|
|
||||||
|
# Initialize outputs
|
||||||
|
CORE_NEEDS_RELEASE="false"
|
||||||
|
FRAMEWORK_NEEDS_RELEASE="false"
|
||||||
|
PLUGINS_NEED_RELEASE="false"
|
||||||
|
BIFROST_HTTP_NEEDS_RELEASE="false"
|
||||||
|
DOCKER_NEEDS_RELEASE="false"
|
||||||
|
CHANGED_PLUGINS="[]"
|
||||||
|
|
||||||
|
# Get current versions
|
||||||
|
CORE_VERSION=$(cat core/version)
|
||||||
|
FRAMEWORK_VERSION=$(cat framework/version)
|
||||||
|
TRANSPORT_VERSION=$(cat transports/version)
|
||||||
|
|
||||||
|
echo "📦 Current versions:"
|
||||||
|
echo " Core: $CORE_VERSION"
|
||||||
|
echo " Framework: $FRAMEWORK_VERSION"
|
||||||
|
echo " Transport: $TRANSPORT_VERSION"
|
||||||
|
|
||||||
|
START_FROM="none"
|
||||||
|
|
||||||
|
# Check Core
|
||||||
|
echo ""
|
||||||
|
echo "🔧 Checking core..."
|
||||||
|
CORE_TAG="core/v${CORE_VERSION}"
|
||||||
|
if git rev-parse --verify "$CORE_TAG" >/dev/null 2>&1; then
|
||||||
|
echo " ⏭️ Tag $CORE_TAG already exists"
|
||||||
|
else
|
||||||
|
# Extract major.minor track from version (e.g., "1.3.55" -> "1.3", "1.4.0-prerelease1" -> "1.4")
|
||||||
|
CORE_BASE_VERSION=$(echo "$CORE_VERSION" | sed 's/-.*$//')
|
||||||
|
CORE_MAJOR_MINOR=$(echo "$CORE_BASE_VERSION" | cut -d. -f1,2)
|
||||||
|
echo " 🔍 Checking track: ${CORE_MAJOR_MINOR}.x"
|
||||||
|
|
||||||
|
# Get previous version in the same track
|
||||||
|
LATEST_CORE_TAG=$(git tag -l "core/v${CORE_MAJOR_MINOR}.*" | sort -V | tail -1)
|
||||||
|
echo "🏷️ Latest core tag in track ${CORE_MAJOR_MINOR}.x: $LATEST_CORE_TAG"
|
||||||
|
if [ -z "$LATEST_CORE_TAG" ]; then
|
||||||
|
echo " ✅ First core release in track ${CORE_MAJOR_MINOR}.x: $CORE_VERSION"
|
||||||
|
CORE_NEEDS_RELEASE="true"
|
||||||
|
else
|
||||||
|
if [[ "$CORE_VERSION" == *"-"* ]]; then
|
||||||
|
# current_version has prerelease, so include all versions but prefer stable
|
||||||
|
ALL_TAGS=$(git tag -l "core/v${CORE_MAJOR_MINOR}.*" | sort -V)
|
||||||
|
STABLE_TAGS=$(echo "$ALL_TAGS" | grep -v '\-' || true)
|
||||||
|
PRERELEASE_TAGS=$(echo "$ALL_TAGS" | grep '\-' || true)
|
||||||
|
if [ -n "$STABLE_TAGS" ]; then
|
||||||
|
# Get the highest stable version
|
||||||
|
LATEST_CORE_TAG=$(echo "$STABLE_TAGS" | tail -1)
|
||||||
|
echo "latest core tag (stable preferred): $LATEST_CORE_TAG"
|
||||||
|
else
|
||||||
|
# No stable versions, get highest prerelease
|
||||||
|
LATEST_CORE_TAG=$(echo "$PRERELEASE_TAGS" | tail -1)
|
||||||
|
echo "latest core tag (prerelease only): $LATEST_CORE_TAG"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# VERSION has no prerelease, so only consider stable releases in same track
|
||||||
|
LATEST_CORE_TAG=$(git tag -l "core/v${CORE_MAJOR_MINOR}.*" | grep -v '\-' | sort -V | tail -1 || true)
|
||||||
|
echo "latest core tag (stable only): $LATEST_CORE_TAG"
|
||||||
|
fi
|
||||||
|
PREVIOUS_CORE_VERSION=${LATEST_CORE_TAG#core/v}
|
||||||
|
echo " 📋 Previous: $PREVIOUS_CORE_VERSION, Current: $CORE_VERSION"
|
||||||
|
# Fixed: Use head -1 instead of tail -1 for your sort -V behavior, and check against current version
|
||||||
|
if [ "$(printf '%s\n' "$PREVIOUS_CORE_VERSION" "$CORE_VERSION" | sort -V | tail -1)" = "$CORE_VERSION" ] && [ "$PREVIOUS_CORE_VERSION" != "$CORE_VERSION" ]; then
|
||||||
|
echo " ✅ Core version incremented: $PREVIOUS_CORE_VERSION → $CORE_VERSION"
|
||||||
|
CORE_NEEDS_RELEASE="true"
|
||||||
|
else
|
||||||
|
echo " ⏭️ No core version increment"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check Framework
|
||||||
|
echo ""
|
||||||
|
echo "📦 Checking framework..."
|
||||||
|
FRAMEWORK_TAG="framework/v${FRAMEWORK_VERSION}"
|
||||||
|
if git rev-parse --verify "$FRAMEWORK_TAG" >/dev/null 2>&1; then
|
||||||
|
echo " ⏭️ Tag $FRAMEWORK_TAG already exists"
|
||||||
|
else
|
||||||
|
# Extract major.minor track from version (e.g., "1.3.55" -> "1.3", "1.4.0-prerelease1" -> "1.4")
|
||||||
|
FRAMEWORK_BASE_VERSION=$(echo "$FRAMEWORK_VERSION" | sed 's/-.*$//')
|
||||||
|
FRAMEWORK_MAJOR_MINOR=$(echo "$FRAMEWORK_BASE_VERSION" | cut -d. -f1,2)
|
||||||
|
echo " 🔍 Checking track: ${FRAMEWORK_MAJOR_MINOR}.x"
|
||||||
|
|
||||||
|
LATEST_FRAMEWORK_TAG=""
|
||||||
|
if [[ "$FRAMEWORK_VERSION" == *"-"* ]]; then
|
||||||
|
# current_version has prerelease, so include all versions but prefer stable
|
||||||
|
ALL_TAGS=$(git tag -l "framework/v${FRAMEWORK_MAJOR_MINOR}.*" | sort -V)
|
||||||
|
STABLE_TAGS=$(echo "$ALL_TAGS" | grep -v '\-' || true)
|
||||||
|
PRERELEASE_TAGS=$(echo "$ALL_TAGS" | grep '\-' || true)
|
||||||
|
if [ -n "$STABLE_TAGS" ]; then
|
||||||
|
# Get the highest stable version
|
||||||
|
LATEST_FRAMEWORK_TAG=$(echo "$STABLE_TAGS" | tail -1)
|
||||||
|
echo "latest framework tag (stable preferred): $LATEST_FRAMEWORK_TAG"
|
||||||
|
else
|
||||||
|
# No stable versions, get highest prerelease
|
||||||
|
LATEST_FRAMEWORK_TAG=$(echo "$PRERELEASE_TAGS" | tail -1)
|
||||||
|
echo "latest framework tag (prerelease only): $LATEST_FRAMEWORK_TAG"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# VERSION has no prerelease, so only consider stable releases in same track
|
||||||
|
LATEST_FRAMEWORK_TAG=$(git tag -l "framework/v${FRAMEWORK_MAJOR_MINOR}.*" | grep -v '\-' | sort -V | tail -1 || true)
|
||||||
|
echo "latest framework tag (stable only): $LATEST_FRAMEWORK_TAG"
|
||||||
|
fi
|
||||||
|
if [ -z "$LATEST_FRAMEWORK_TAG" ]; then
|
||||||
|
echo " ✅ First framework release in track ${FRAMEWORK_MAJOR_MINOR}.x: $FRAMEWORK_VERSION"
|
||||||
|
FRAMEWORK_NEEDS_RELEASE="true"
|
||||||
|
else
|
||||||
|
PREVIOUS_FRAMEWORK_VERSION=${LATEST_FRAMEWORK_TAG#framework/v}
|
||||||
|
echo " 📋 Previous: $PREVIOUS_FRAMEWORK_VERSION, Current: $FRAMEWORK_VERSION"
|
||||||
|
# Fixed: Use head -1 instead of tail -1 for your sort -V behavior, and check against current version
|
||||||
|
if [ "$(printf '%s\n' "$PREVIOUS_FRAMEWORK_VERSION" "$FRAMEWORK_VERSION" | sort -V | tail -1)" = "$FRAMEWORK_VERSION" ] && [ "$PREVIOUS_FRAMEWORK_VERSION" != "$FRAMEWORK_VERSION" ]; then
|
||||||
|
echo " ✅ Framework version incremented: $PREVIOUS_FRAMEWORK_VERSION → $FRAMEWORK_VERSION"
|
||||||
|
FRAMEWORK_NEEDS_RELEASE="true"
|
||||||
|
else
|
||||||
|
echo " ⏭️ No framework version increment"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check Plugins
|
||||||
|
echo ""
|
||||||
|
echo "🔌 Checking plugins..."
|
||||||
|
PLUGIN_CHANGES=()
|
||||||
|
|
||||||
|
for plugin_dir in plugins/*/; do
|
||||||
|
if [ ! -d "$plugin_dir" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
plugin_name=$(basename "$plugin_dir")
|
||||||
|
version_file="${plugin_dir}version"
|
||||||
|
|
||||||
|
if [ ! -f "$version_file" ]; then
|
||||||
|
echo " ⚠️ No version file for: $plugin_name"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
current_version=$(cat "$version_file" | tr -d '\n\r')
|
||||||
|
if [ -z "$current_version" ]; then
|
||||||
|
echo " ⚠️ Empty version file for: $plugin_name"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
tag_name="plugins/${plugin_name}/v${current_version}"
|
||||||
|
echo " 📦 Plugin: $plugin_name (v$current_version)"
|
||||||
|
|
||||||
|
if git rev-parse --verify "$tag_name" >/dev/null 2>&1; then
|
||||||
|
echo " ⏭️ Tag already exists"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract major.minor track from version (e.g., "1.3.55" -> "1.3", "1.4.0-prerelease1" -> "1.4")
|
||||||
|
plugin_base_version=$(echo "$current_version" | sed 's/-.*$//')
|
||||||
|
plugin_major_minor=$(echo "$plugin_base_version" | cut -d. -f1,2)
|
||||||
|
echo " 🔍 Checking track: ${plugin_major_minor}.x"
|
||||||
|
|
||||||
|
if [[ "$current_version" == *"-"* ]]; then
|
||||||
|
# current_version has prerelease, so include all versions but prefer stable
|
||||||
|
ALL_TAGS=$(git tag -l "plugins/${plugin_name}/v${plugin_major_minor}.*" | sort -V)
|
||||||
|
STABLE_TAGS=$(echo "$ALL_TAGS" | grep -v '\-' || true)
|
||||||
|
PRERELEASE_TAGS=$(echo "$ALL_TAGS" | grep '\-' || true)
|
||||||
|
|
||||||
|
if [ -n "$STABLE_TAGS" ]; then
|
||||||
|
# Get the highest stable version
|
||||||
|
LATEST_PLUGIN_TAG=$(echo "$STABLE_TAGS" | tail -1)
|
||||||
|
echo "latest plugin tag (stable preferred): $LATEST_PLUGIN_TAG"
|
||||||
|
else
|
||||||
|
# No stable versions, get highest prerelease
|
||||||
|
LATEST_PLUGIN_TAG=$(echo "$PRERELEASE_TAGS" | tail -1)
|
||||||
|
echo "latest plugin tag (prerelease only): $LATEST_PLUGIN_TAG"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# VERSION has no prerelease, so only consider stable releases in same track
|
||||||
|
LATEST_PLUGIN_TAG=$(git tag -l "plugins/${plugin_name}/v${plugin_major_minor}.*" | grep -v '\-' | sort -V | tail -1 || true)
|
||||||
|
echo "latest plugin tag (stable only): $LATEST_PLUGIN_TAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
latest_tag=$LATEST_PLUGIN_TAG
|
||||||
|
if [ -z "$latest_tag" ]; then
|
||||||
|
echo " ✅ First release in track ${plugin_major_minor}.x"
|
||||||
|
PLUGIN_CHANGES+=("$plugin_name")
|
||||||
|
else
|
||||||
|
previous_version=${latest_tag#plugins/${plugin_name}/v}
|
||||||
|
echo "previous version: $previous_version"
|
||||||
|
echo "current version: $current_version"
|
||||||
|
echo "latest tag: $latest_tag"
|
||||||
|
if [ "$(printf '%s\n' "$previous_version" "$current_version" | sort -V | tail -1)" = "$current_version" ] && [ "$previous_version" != "$current_version" ]; then
|
||||||
|
echo " ✅ Version incremented: $previous_version → $current_version"
|
||||||
|
PLUGIN_CHANGES+=("$plugin_name")
|
||||||
|
else
|
||||||
|
echo " ⏭️ No version increment"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#PLUGIN_CHANGES[@]} -gt 0 ]; then
|
||||||
|
PLUGINS_NEED_RELEASE="true"
|
||||||
|
echo " 🔄 Plugins with changes: ${PLUGIN_CHANGES[*]}"
|
||||||
|
else
|
||||||
|
echo " ⏭️ No plugin changes detected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check Bifrost HTTP
|
||||||
|
echo ""
|
||||||
|
echo "🚀 Checking bifrost-http..."
|
||||||
|
TRANSPORT_TAG="transports/v${TRANSPORT_VERSION}"
|
||||||
|
DOCKER_TAG_EXISTS="false"
|
||||||
|
|
||||||
|
# Check if Git tag exists
|
||||||
|
GIT_TAG_EXISTS="false"
|
||||||
|
if git rev-parse --verify "$TRANSPORT_TAG" >/dev/null 2>&1; then
|
||||||
|
echo " ⏭️ Git tag $TRANSPORT_TAG already exists"
|
||||||
|
GIT_TAG_EXISTS="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if Docker tag exists on DockerHub
|
||||||
|
echo " 🐳 Checking DockerHub for tag v${TRANSPORT_VERSION}..."
|
||||||
|
DOCKER_CHECK_RESPONSE=$(curl -s "https://registry.hub.docker.com/v2/repositories/maximhq/bifrost/tags/v${TRANSPORT_VERSION}/" 2>/dev/null || echo "")
|
||||||
|
if [ -n "$DOCKER_CHECK_RESPONSE" ] && echo "$DOCKER_CHECK_RESPONSE" | grep -q '"name"'; then
|
||||||
|
echo " ⏭️ Docker tag v${TRANSPORT_VERSION} already exists on DockerHub"
|
||||||
|
DOCKER_TAG_EXISTS="true"
|
||||||
|
else
|
||||||
|
echo " ❌ Docker tag v${TRANSPORT_VERSION} not found on DockerHub"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine if release is needed
|
||||||
|
if [ "$GIT_TAG_EXISTS" = "true" ] && [ "$DOCKER_TAG_EXISTS" = "true" ]; then
|
||||||
|
echo " ⏭️ Both Git tag and Docker image exist - no release needed"
|
||||||
|
else
|
||||||
|
# Extract major.minor track from version (e.g., "1.3.55" -> "1.3", "1.4.0-prerelease1" -> "1.4")
|
||||||
|
TRANSPORT_BASE_VERSION=$(echo "$TRANSPORT_VERSION" | sed 's/-.*$//')
|
||||||
|
TRANSPORT_MAJOR_MINOR=$(echo "$TRANSPORT_BASE_VERSION" | cut -d. -f1,2)
|
||||||
|
echo " 🔍 Checking track: ${TRANSPORT_MAJOR_MINOR}.x"
|
||||||
|
|
||||||
|
# Get all transport tags in the same track, prioritize stable over prerelease for same base version
|
||||||
|
ALL_TRANSPORT_TAGS=$(git tag -l "transports/v${TRANSPORT_MAJOR_MINOR}.*" | sort -V)
|
||||||
|
|
||||||
|
# Function to get base version (remove prerelease suffix)
|
||||||
|
get_base_version() {
|
||||||
|
echo "$1" | sed 's/-.*$//'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find the latest version, prioritizing stable over prerelease
|
||||||
|
LATEST_TRANSPORT_TAG=""
|
||||||
|
LATEST_BASE_VERSION=""
|
||||||
|
|
||||||
|
for tag in $ALL_TRANSPORT_TAGS; do
|
||||||
|
version=${tag#transports/v}
|
||||||
|
base_version=$(get_base_version "$version")
|
||||||
|
|
||||||
|
# If this base version is newer, or same base version but current is stable and we had prerelease
|
||||||
|
if [ -z "$LATEST_BASE_VERSION" ] || \
|
||||||
|
[ "$(printf '%s\n' "$LATEST_BASE_VERSION" "$base_version" | sort -V | tail -1)" = "$base_version" ]; then
|
||||||
|
|
||||||
|
if [ "$base_version" = "$LATEST_BASE_VERSION" ]; then
|
||||||
|
# Same base version - prefer stable (no hyphen) over prerelease, otherwise take the later one
|
||||||
|
if [[ "$version" != *"-"* ]]; then
|
||||||
|
# Current is stable, always prefer it
|
||||||
|
LATEST_TRANSPORT_TAG="$tag"
|
||||||
|
elif [[ "${LATEST_TRANSPORT_TAG#transports/v}" == *"-"* ]]; then
|
||||||
|
# Both are prereleases, take the later one (thanks to sort -V)
|
||||||
|
LATEST_TRANSPORT_TAG="$tag"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# New base version is higher
|
||||||
|
LATEST_TRANSPORT_TAG="$tag"
|
||||||
|
LATEST_BASE_VERSION="$base_version"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$LATEST_TRANSPORT_TAG" ]; then
|
||||||
|
echo " 🏷️ Latest transport tag: $LATEST_TRANSPORT_TAG"
|
||||||
|
fi
|
||||||
|
if [ -z "$LATEST_TRANSPORT_TAG" ]; then
|
||||||
|
echo " ✅ First transport release in track ${TRANSPORT_MAJOR_MINOR}.x: $TRANSPORT_VERSION"
|
||||||
|
if [ "$GIT_TAG_EXISTS" = "false" ]; then
|
||||||
|
echo " 🏷️ Git tag missing - transport release needed"
|
||||||
|
BIFROST_HTTP_NEEDS_RELEASE="true"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
PREVIOUS_TRANSPORT_VERSION=${LATEST_TRANSPORT_TAG#transports/v}
|
||||||
|
echo " 📋 Previous: $PREVIOUS_TRANSPORT_VERSION, Current: $TRANSPORT_VERSION"
|
||||||
|
|
||||||
|
# Function to compare versions with proper prerelease handling
|
||||||
|
# Returns 0 if $1 < $2, 1 otherwise
|
||||||
|
version_less_than() {
|
||||||
|
local v1="$1"
|
||||||
|
local v2="$2"
|
||||||
|
|
||||||
|
# Extract base versions (remove prerelease suffix)
|
||||||
|
local base1=$(echo "$v1" | sed 's/-.*$//')
|
||||||
|
local base2=$(echo "$v2" | sed 's/-.*$//')
|
||||||
|
|
||||||
|
# Compare base versions
|
||||||
|
if [ "$base1" != "$base2" ]; then
|
||||||
|
# Different base versions, use sort -V
|
||||||
|
[ "$(printf '%s\n' "$base1" "$base2" | sort -V | head -1)" = "$base1" ]
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Same base version, check prereleases
|
||||||
|
local pre1=$(echo "$v1" | grep -o '\-.*$' || echo "")
|
||||||
|
local pre2=$(echo "$v2" | grep -o '\-.*$' || echo "")
|
||||||
|
|
||||||
|
if [ -z "$pre1" ] && [ -n "$pre2" ]; then
|
||||||
|
# v1 is stable, v2 is prerelease: v2 < v1
|
||||||
|
return 1
|
||||||
|
elif [ -n "$pre1" ] && [ -z "$pre2" ]; then
|
||||||
|
# v1 is prerelease, v2 is stable: v1 < v2
|
||||||
|
return 0
|
||||||
|
elif [ -n "$pre1" ] && [ -n "$pre2" ]; then
|
||||||
|
# Both prereleases, compare them
|
||||||
|
[ "$(printf '%s\n' "$pre1" "$pre2" | sort -V | head -1)" = "$pre1" ]
|
||||||
|
return $?
|
||||||
|
else
|
||||||
|
# Both stable and same base: equal
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if current version is greater than previous
|
||||||
|
if version_less_than "$PREVIOUS_TRANSPORT_VERSION" "$TRANSPORT_VERSION"; then
|
||||||
|
echo " ✅ Transport version incremented: $PREVIOUS_TRANSPORT_VERSION → $TRANSPORT_VERSION"
|
||||||
|
if [ "$GIT_TAG_EXISTS" = "false" ]; then
|
||||||
|
echo " 🏷️ Git tag missing - transport release needed"
|
||||||
|
BIFROST_HTTP_NEEDS_RELEASE="true"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ⏭️ No transport version increment"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if Docker image needs to be built (independent of transport release)
|
||||||
|
if [ "$DOCKER_TAG_EXISTS" = "false" ]; then
|
||||||
|
echo " 🐳 Docker image missing - docker release needed"
|
||||||
|
DOCKER_NEEDS_RELEASE="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Convert plugin array to JSON (compact format)
|
||||||
|
if [ ${#PLUGIN_CHANGES[@]} -eq 0 ]; then
|
||||||
|
CHANGED_PLUGINS_JSON="[]"
|
||||||
|
else
|
||||||
|
CHANGED_PLUGINS_JSON=$(printf '%s\n' "${PLUGIN_CHANGES[@]}" | jq -R . | jq -s -c .)
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "CHANGED_PLUGINS_JSON: $CHANGED_PLUGINS_JSON"
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo ""
|
||||||
|
echo "📋 Release Summary:"
|
||||||
|
echo " Core: $CORE_NEEDS_RELEASE (v$CORE_VERSION)"
|
||||||
|
echo " Framework: $FRAMEWORK_NEEDS_RELEASE (v$FRAMEWORK_VERSION)"
|
||||||
|
echo " Plugins: $PLUGINS_NEED_RELEASE (${#PLUGIN_CHANGES[@]} plugins)"
|
||||||
|
echo " Bifrost HTTP: $BIFROST_HTTP_NEEDS_RELEASE (v$TRANSPORT_VERSION)"
|
||||||
|
echo " Docker: $DOCKER_NEEDS_RELEASE (v$TRANSPORT_VERSION)"
|
||||||
|
|
||||||
|
# Set outputs (only when running in GitHub Actions)
|
||||||
|
if [ -n "${GITHUB_OUTPUT:-}" ]; then
|
||||||
|
{
|
||||||
|
echo "core-needs-release=$CORE_NEEDS_RELEASE"
|
||||||
|
echo "framework-needs-release=$FRAMEWORK_NEEDS_RELEASE"
|
||||||
|
echo "plugins-need-release=$PLUGINS_NEED_RELEASE"
|
||||||
|
echo "bifrost-http-needs-release=$BIFROST_HTTP_NEEDS_RELEASE"
|
||||||
|
echo "docker-needs-release=$DOCKER_NEEDS_RELEASE"
|
||||||
|
echo "changed-plugins=$CHANGED_PLUGINS_JSON"
|
||||||
|
echo "core-version=$CORE_VERSION"
|
||||||
|
echo "framework-version=$FRAMEWORK_VERSION"
|
||||||
|
echo "transport-version=$TRANSPORT_VERSION"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "ℹ️ GITHUB_OUTPUT not set; skipping outputs write (local run)"
|
||||||
|
fi
|
||||||
41
.github/workflows/scripts/extract-npx-version.sh
vendored
Executable file
41
.github/workflows/scripts/extract-npx-version.sh
vendored
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Extract NPX version from package.json
|
||||||
|
# Usage: ./extract-npx-version.sh
|
||||||
|
|
||||||
|
# Path to package.json
|
||||||
|
PACKAGE_JSON="npx/bifrost/package.json"
|
||||||
|
|
||||||
|
if [[ ! -f "${PACKAGE_JSON}" ]]; then
|
||||||
|
echo "❌ package.json not found at ${PACKAGE_JSON}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "📋 Reading version from ${PACKAGE_JSON}"
|
||||||
|
|
||||||
|
# Extract version from package.json using jq
|
||||||
|
VERSION=$(jq -r '.version' "${PACKAGE_JSON}")
|
||||||
|
|
||||||
|
if [[ -z "${VERSION}" ]] || [[ "${VERSION}" == "null" ]]; then
|
||||||
|
echo "❌ Failed to extract version from package.json"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate version format (X.Y.Z or prerelease like X.Y.Z-rc.1)
|
||||||
|
if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
|
||||||
|
echo "❌ Invalid version format '${VERSION}'. Expected format: MAJOR.MINOR.PATCH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "📦 Extracted NPX version: ${VERSION}"
|
||||||
|
|
||||||
|
# Set outputs (only when running in GitHub Actions)
|
||||||
|
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
|
||||||
|
{
|
||||||
|
echo "version=${VERSION}"
|
||||||
|
echo "full-tag=npx/bifrost/v${VERSION}"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "::notice::GITHUB_OUTPUT not set; skipping outputs (local run?)"
|
||||||
|
fi
|
||||||
72
.github/workflows/scripts/get_curls.sh
vendored
Executable file
72
.github/workflows/scripts/get_curls.sh
vendored
Executable file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
# Bifrost HTTP Transport - GET API Endpoints
|
||||||
|
# This script tests all GET endpoints and reports their status
|
||||||
|
|
||||||
|
# Base URL (update as needed)
|
||||||
|
BASE_URL="${BASE_URL:-http://localhost:8080}"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Track failures
|
||||||
|
FAILED_TESTS=0
|
||||||
|
TOTAL_TESTS=0
|
||||||
|
|
||||||
|
echo "Bifrost GET API Endpoints - Status Check"
|
||||||
|
echo "========================================"
|
||||||
|
echo "Base URL: $BASE_URL"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Function to test endpoint
|
||||||
|
test_endpoint() {
|
||||||
|
local path=$1
|
||||||
|
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||||
|
local status=$(curl -s -o /dev/null -w "%{http_code}" -X GET "$BASE_URL$path" -H "Content-Type: application/json")
|
||||||
|
|
||||||
|
if [ "$status" -ge 200 ] && [ "$status" -lt 300 ]; then
|
||||||
|
echo -e "GET $path - ${GREEN}✓ SUCCESS${NC} ($status)"
|
||||||
|
else
|
||||||
|
echo -e "GET $path - ${RED}✗ FAILURE${NC} ($status)"
|
||||||
|
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test all endpoints
|
||||||
|
test_endpoint "/health"
|
||||||
|
test_endpoint "/api/session/is-auth-enabled"
|
||||||
|
test_endpoint "/api/plugins"
|
||||||
|
test_endpoint "/api/plugins/telemetry"
|
||||||
|
test_endpoint "/api/mcp/clients"
|
||||||
|
test_endpoint "/api/logs?limit=10&offset=0&sort_by=timestamp&order=desc"
|
||||||
|
test_endpoint "/api/logs/dropped"
|
||||||
|
test_endpoint "/api/logs/filterdata"
|
||||||
|
test_endpoint "/api/providers"
|
||||||
|
test_endpoint "/api/providers/openai"
|
||||||
|
test_endpoint "/api/keys"
|
||||||
|
test_endpoint "/api/governance/virtual-keys"
|
||||||
|
test_endpoint "/api/governance/virtual-keys/vk-123"
|
||||||
|
test_endpoint "/api/governance/teams"
|
||||||
|
test_endpoint "/api/governance/teams/team-123"
|
||||||
|
test_endpoint "/api/governance/customers"
|
||||||
|
test_endpoint "/api/governance/customers/cust-123"
|
||||||
|
test_endpoint "/api/config"
|
||||||
|
test_endpoint "/api/config?from_db=true"
|
||||||
|
test_endpoint "/api/version"
|
||||||
|
test_endpoint "/v1/models"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Note: WebSocket endpoint (/ws) requires a WebSocket client${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "Test Summary:"
|
||||||
|
echo " Total tests: $TOTAL_TESTS"
|
||||||
|
echo " Passed: $((TOTAL_TESTS - FAILED_TESTS))"
|
||||||
|
echo " Failed: $FAILED_TESTS"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
echo "The aim of the script is to make sure bifrost server is not crashing"
|
||||||
45
.github/workflows/scripts/go-utils.sh
vendored
Executable file
45
.github/workflows/scripts/go-utils.sh
vendored
Executable file
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Shared utilities for Go operations in release scripts
|
||||||
|
# Usage: source .github/workflows/scripts/go-utils.sh
|
||||||
|
|
||||||
|
# Function to perform go get with exponential backoff
|
||||||
|
# Usage: go_get_with_backoff <package@version>
|
||||||
|
go_get_with_backoff() {
|
||||||
|
local package="$1"
|
||||||
|
local max_attempts=30
|
||||||
|
local initial_wait=30
|
||||||
|
local max_wait=120 # 2 minutes
|
||||||
|
local attempt=1
|
||||||
|
local wait_time=$initial_wait
|
||||||
|
|
||||||
|
echo "🔄 Attempting to get $package with exponential backoff..."
|
||||||
|
|
||||||
|
while [ $attempt -le $max_attempts ]; do
|
||||||
|
echo "📦 Attempt $attempt/$max_attempts: go get $package"
|
||||||
|
|
||||||
|
if go get "$package"; then
|
||||||
|
echo "✅ Successfully retrieved $package on attempt $attempt"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $attempt -eq $max_attempts ]; then
|
||||||
|
echo "❌ Failed to get $package after $max_attempts attempts"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "⏳ Waiting ${wait_time}s before retry (attempt $attempt/$max_attempts failed)..."
|
||||||
|
sleep $wait_time
|
||||||
|
|
||||||
|
# Calculate next wait time (exponential backoff)
|
||||||
|
# Double the wait time, but cap at max_wait
|
||||||
|
wait_time=$((wait_time * 2))
|
||||||
|
if [ $wait_time -gt $max_wait ]; then
|
||||||
|
wait_time=$max_wait
|
||||||
|
fi
|
||||||
|
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
81
.github/workflows/scripts/install-cross-compilers.sh
vendored
Executable file
81
.github/workflows/scripts/install-cross-compilers.sh
vendored
Executable file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Install cross-compilation toolchains for Go + CGO
|
||||||
|
# Usage: ./install-cross-compilers.sh
|
||||||
|
|
||||||
|
echo "📦 Installing cross-compilation toolchains for Go + CGO..."
|
||||||
|
|
||||||
|
# Install all required packages
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
gcc-x86-64-linux-gnu \
|
||||||
|
gcc-aarch64-linux-gnu \
|
||||||
|
gcc-mingw-w64-x86-64 \
|
||||||
|
musl-tools \
|
||||||
|
clang \
|
||||||
|
lld \
|
||||||
|
xz-utils \
|
||||||
|
curl
|
||||||
|
|
||||||
|
# Create symbolic links for musl compilers
|
||||||
|
sudo ln -sf /usr/bin/x86_64-linux-gnu-gcc /usr/local/bin/x86_64-linux-musl-gcc
|
||||||
|
sudo ln -sf /usr/bin/x86_64-linux-gnu-g++ /usr/local/bin/x86_64-linux-musl-g++
|
||||||
|
sudo ln -sf /usr/bin/aarch64-linux-gnu-gcc /usr/local/bin/aarch64-linux-musl-gcc
|
||||||
|
sudo ln -sf /usr/bin/aarch64-linux-gnu-g++ /usr/local/bin/aarch64-linux-musl-g++
|
||||||
|
|
||||||
|
echo "🍎 Setting up Darwin cross-compilation..."
|
||||||
|
|
||||||
|
# Where to install SDK
|
||||||
|
SDK_DIR="/opt/MacOSX12.3.sdk"
|
||||||
|
SDK_URL="https://github.com/phracker/MacOSX-SDKs/releases/download/12.3/MacOSX12.3.sdk.tar.xz"
|
||||||
|
|
||||||
|
# Download and extract macOS SDK if not already installed
|
||||||
|
if [ ! -d "$SDK_DIR" ]; then
|
||||||
|
echo "📦 Downloading macOS SDK..."
|
||||||
|
# Use -f to fail on HTTP errors, -L to follow redirects
|
||||||
|
if ! curl -fL "$SDK_URL" -o /tmp/MacOSX12.3.sdk.tar.xz; then
|
||||||
|
echo "❌ Failed to download macOS SDK from primary URL, trying alternative..."
|
||||||
|
SDK_URL_ALT="https://github.com/joseluisq/macosx-sdks/releases/download/12.3/MacOSX12.3.sdk.tar.xz"
|
||||||
|
curl -fL "$SDK_URL_ALT" -o /tmp/MacOSX12.3.sdk.tar.xz
|
||||||
|
fi
|
||||||
|
sudo mkdir -p /opt
|
||||||
|
sudo tar -xf /tmp/MacOSX12.3.sdk.tar.xz -C /opt
|
||||||
|
rm -f /tmp/MacOSX12.3.sdk.tar.xz
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create wrapper scripts with proper shebang and linker configuration
|
||||||
|
sudo tee /usr/local/bin/o64-clang > /dev/null << 'WRAPPER_EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
exec clang -target x86_64-apple-darwin --sysroot=/opt/MacOSX12.3.sdk -fuse-ld=lld -Wno-unused-command-line-argument "$@"
|
||||||
|
WRAPPER_EOF
|
||||||
|
|
||||||
|
sudo tee /usr/local/bin/o64-clang++ > /dev/null << 'WRAPPER_EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
exec clang++ -target x86_64-apple-darwin --sysroot=/opt/MacOSX12.3.sdk -fuse-ld=lld -Wno-unused-command-line-argument "$@"
|
||||||
|
WRAPPER_EOF
|
||||||
|
|
||||||
|
sudo tee /usr/local/bin/oa64-clang > /dev/null << 'WRAPPER_EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
exec clang -target arm64-apple-darwin --sysroot=/opt/MacOSX12.3.sdk -fuse-ld=lld -Wno-unused-command-line-argument "$@"
|
||||||
|
WRAPPER_EOF
|
||||||
|
|
||||||
|
sudo tee /usr/local/bin/oa64-clang++ > /dev/null << 'WRAPPER_EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
exec clang++ -target arm64-apple-darwin --sysroot=/opt/MacOSX12.3.sdk -fuse-ld=lld -Wno-unused-command-line-argument "$@"
|
||||||
|
WRAPPER_EOF
|
||||||
|
|
||||||
|
sudo chmod +x /usr/local/bin/o64-clang /usr/local/bin/o64-clang++ \
|
||||||
|
/usr/local/bin/oa64-clang /usr/local/bin/oa64-clang++
|
||||||
|
|
||||||
|
echo "✅ Darwin cross-compilation environment ready!"
|
||||||
|
|
||||||
|
echo "✅ Cross-compilation toolchains installed"
|
||||||
|
echo ""
|
||||||
|
echo "Available cross-compilers:"
|
||||||
|
echo " Linux amd64: x86_64-linux-musl-gcc, x86_64-linux-musl-g++"
|
||||||
|
echo " Linux arm64: aarch64-linux-musl-gcc, aarch64-linux-musl-g++"
|
||||||
|
echo " Windows amd64: x86_64-w64-mingw32-gcc, x86_64-w64-mingw32-g++"
|
||||||
|
echo " Windows arm64: aarch64-w64-mingw32-gcc, aarch64-w64-mingw32-g++"
|
||||||
|
echo " Darwin amd64: o64-clang, o64-clang++"
|
||||||
|
echo " Darwin arm64: oa64-clang, oa64-clang++"
|
||||||
39
.github/workflows/scripts/load-test-results.json
vendored
Normal file
39
.github/workflows/scripts/load-test-results.json
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"overhead": {
|
||||||
|
"configured_rate": 1000,
|
||||||
|
"actual_rate": 1000,
|
||||||
|
"duration": 30,
|
||||||
|
"concurrent": 1000,
|
||||||
|
"success_rate": 100.00,
|
||||||
|
"latency_us": {
|
||||||
|
"min": 999926.96,
|
||||||
|
"mean": 849.31,
|
||||||
|
"p50": 155.78,
|
||||||
|
"p90": 408.13,
|
||||||
|
"p95": 636.92,
|
||||||
|
"p99": 4526.28,
|
||||||
|
"max": 176968.29
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp": "2026-02-14T12:40:01Z",
|
||||||
|
"stress": {
|
||||||
|
"rate": 1000,
|
||||||
|
"duration": 30,
|
||||||
|
"mocker_latency_ms": 1000,
|
||||||
|
"success_rate": 100.00
|
||||||
|
},
|
||||||
|
"process_stats": {
|
||||||
|
"overhead": {
|
||||||
|
"cpu_avg_pct": 20.7,
|
||||||
|
"cpu_peak_pct": 66.9,
|
||||||
|
"rss_avg_mb": 332.8,
|
||||||
|
"rss_peak_mb": 640.2
|
||||||
|
},
|
||||||
|
"stress": {
|
||||||
|
"cpu_avg_pct": 29.9,
|
||||||
|
"cpu_peak_pct": 73.5,
|
||||||
|
"rss_avg_mb": 639.5,
|
||||||
|
"rss_peak_mb": 789.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
.github/workflows/scripts/load-test-results.md
vendored
Normal file
42
.github/workflows/scripts/load-test-results.md
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Bifrost Load Test Results (single instance, 1000 RPS)
|
||||||
|
|
||||||
|
## Bifrost Processing Overhead
|
||||||
|
|
||||||
|
| Metric | Actual RPS | Duration | Concurrent | Success Rate | Min | Mean | P50 | P90 | P95 | P99 | Max |
|
||||||
|
|--------|-----------|----------|------------|--------------|-----|------|-----|-----|-----|-----|-----|
|
||||||
|
| Overhead | 1000 | 30s | ~1000 | 100.00% | 999926.96µs | 849.31µs | 155.78µs | 408.13µs | 636.92µs | 4526.28µs | 176968.29µs |
|
||||||
|
|
||||||
|
## Stress #1 (1000ms mocker latency)
|
||||||
|
|
||||||
|
| Metric | Actual RPS | Duration | Concurrent | Success Rate | Min | Mean | P50 | P90 | P95 | P99 | Max |
|
||||||
|
|--------|-----------|----------|------------|--------------|-----|------|-----|-----|-----|-----|-----|
|
||||||
|
| Stress #1 | 1000 | 30s | ~1000 | 100.00% | 1000.20ms | 1002.58ms | 1000.64ms | 1001.17ms | 1001.67ms | 1047.60ms | 1286.46ms |
|
||||||
|
|
||||||
|
## Stress #2 (1000ms mocker latency)
|
||||||
|
|
||||||
|
| Metric | Actual RPS | Duration | Concurrent | Success Rate | Min | Mean | P50 | P90 | P95 | P99 | Max |
|
||||||
|
|--------|-----------|----------|------------|--------------|-----|------|-----|-----|-----|-----|-----|
|
||||||
|
| Stress #2 | 1000 | 30s | ~1000 | 100.00% | 1000.21ms | 1001.99ms | 1000.54ms | 1000.96ms | 1001.32ms | 1049.41ms | 1252.60ms |
|
||||||
|
|
||||||
|
## Bifrost Process Stats (single instance)
|
||||||
|
|
||||||
|
| Phase | CPU Avg | CPU Peak | RSS Avg | RSS Peak |
|
||||||
|
|-------|---------|----------|---------|----------|
|
||||||
|
| Overhead | 20.7% | 66.9% | 332.8MB | 640.2MB |
|
||||||
|
| Stress | 29.9% | 73.5% | 639.5MB | 789.2MB |
|
||||||
|
|
||||||
|
## Method
|
||||||
|
|
||||||
|
- **Single instance**: All tests run against one bifrost-http process at 1000 RPS
|
||||||
|
- **Overhead measurement**: Mocker at 1000ms latency, calibration (Vegeta->Mocker) subtracted from test (Vegeta->Bifrost->Mocker)
|
||||||
|
- **Stress test**: Mocker at 1000ms latency, verifies 100% success under sustained concurrency
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Overhead values are in microseconds (µs), stress test values in milliseconds (ms)
|
||||||
|
- Overhead ignores the mocker jitter, local network request queuing. In real-world the P99 overhead will be approximately 100 microseconds.
|
||||||
|
- Tiered overhead thresholds: mean<5000µs, p50<5000µs, p90<10000µs, p95<20000µs, p99<100000µs
|
||||||
|
- P50/P90/P95/P99 represent percentile latencies
|
||||||
|
|
||||||
|
---
|
||||||
|
*Generated by Bifrost Load Test Script*
|
||||||
850
.github/workflows/scripts/load-test.sh
vendored
Executable file
850
.github/workflows/scripts/load-test.sh
vendored
Executable file
@@ -0,0 +1,850 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Load Test Script for Bifrost
|
||||||
|
# Runs a load test against bifrost-http with a mocker provider
|
||||||
|
# Usage: ./load-test.sh
|
||||||
|
#
|
||||||
|
# This script:
|
||||||
|
# 1. Builds bifrost-http and mocker locally
|
||||||
|
# 2. Creates a config.json with mocker provider (OpenAI-style)
|
||||||
|
# 3. Starts mocker with 0ms latency and bifrost-http
|
||||||
|
# 4. Runs a calibration (Vegeta -> Mocker direct) to measure Vegeta+network baseline
|
||||||
|
# 5. Runs the overhead test (Vegeta -> Bifrost -> Mocker) to measure total
|
||||||
|
# 6. Subtracts calibration from test to isolate Bifrost proxy overhead
|
||||||
|
# (includes local network hop, JSON parsing/unparsing, plugins, and mocker jitter)
|
||||||
|
# 7. Restarts mocker with 10s latency for a sustained concurrency stress test
|
||||||
|
# 8. Asserts overhead < tiered thresholds (per percentile) and stress test has 100% success rate
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||||
|
BIFROST_HTTP_DIR="${REPO_ROOT}/transports/bifrost-http"
|
||||||
|
TRANSPORTS_DIR="${REPO_ROOT}/transports"
|
||||||
|
WORK_DIR="${SCRIPT_DIR}"
|
||||||
|
MOCKER_DIR="${REPO_ROOT}/../bifrost-benchmarking/mocker"
|
||||||
|
|
||||||
|
BIFROST_PORT=8080
|
||||||
|
MOCKER_PORT=8000
|
||||||
|
RATE=1000
|
||||||
|
MAX_WORKERS=12000
|
||||||
|
OVERHEAD_DURATION=30 # overhead measurement duration (seconds)
|
||||||
|
STRESS_DURATION=30 # stress test duration (seconds)
|
||||||
|
OVERHEAD_MOCKER_LATENCY_MS=1000 # 1 second latency for overhead measurement
|
||||||
|
STRESS_MOCKER_LATENCY_MS=1000 # 1 second latency for stress test
|
||||||
|
# Tiered overhead thresholds (µs) — these cover the full proxy cost:
|
||||||
|
# local network hop, JSON parsing/unparsing, plugins, and mocker jitter.
|
||||||
|
# At ${RATE} RPS × ${OVERHEAD_MOCKER_LATENCY_MS}ms latency ≈ 1000 concurrent requests.
|
||||||
|
MAX_OVERHEAD_MEAN_US=5000 # mean overhead threshold (5ms)
|
||||||
|
MAX_OVERHEAD_P50_US=5000 # p50 overhead threshold (5ms)
|
||||||
|
MAX_OVERHEAD_P90_US=10000 # p90 overhead threshold (10ms)
|
||||||
|
MAX_OVERHEAD_P95_US=20000 # p95 overhead threshold (20ms)
|
||||||
|
MAX_OVERHEAD_P99_US=100000 # p99 overhead threshold (100ms)
|
||||||
|
|
||||||
|
# Results storage for summary table
|
||||||
|
RESULTS_FILE="${WORK_DIR}/load-test-results.md"
|
||||||
|
RESULTS_JSON="${WORK_DIR}/load-test-results.json"
|
||||||
|
|
||||||
|
# Process stats monitoring
|
||||||
|
STATS_PID=""
|
||||||
|
STATS_FILE="${WORK_DIR}/bifrost-stats.csv"
|
||||||
|
|
||||||
|
# Overhead-phase process stats (saved before bifrost restart)
|
||||||
|
OVERHEAD_STATS_CPU_AVG=""
|
||||||
|
OVERHEAD_STATS_CPU_PEAK=""
|
||||||
|
OVERHEAD_STATS_RSS_AVG=""
|
||||||
|
OVERHEAD_STATS_RSS_PEAK=""
|
||||||
|
|
||||||
|
# Calibration results per bucket (Vegeta -> Mocker direct)
|
||||||
|
CAL_MIN_NS=0
|
||||||
|
CAL_MEAN_NS=0
|
||||||
|
CAL_50_NS=0
|
||||||
|
CAL_90_NS=0
|
||||||
|
CAL_95_NS=0
|
||||||
|
CAL_99_NS=0
|
||||||
|
CAL_MAX_NS=0
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
log_info() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warn() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cleanup function to kill background processes
|
||||||
|
cleanup() {
|
||||||
|
log_info "Cleaning up..."
|
||||||
|
if [ -n "$STATS_PID" ] && kill -0 "$STATS_PID" 2>/dev/null; then
|
||||||
|
kill "$STATS_PID" 2>/dev/null || true
|
||||||
|
wait "$STATS_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [ -n "$BIFROST_PID" ] && kill -0 "$BIFROST_PID" 2>/dev/null; then
|
||||||
|
kill "$BIFROST_PID" 2>/dev/null || true
|
||||||
|
wait "$BIFROST_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [ -n "$MOCKER_PID" ] && kill -0 "$MOCKER_PID" 2>/dev/null; then
|
||||||
|
kill "$MOCKER_PID" 2>/dev/null || true
|
||||||
|
wait "$MOCKER_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
# Clean up temporary files (keep results files for artifact upload)
|
||||||
|
rm -f "${WORK_DIR}/config.json" "${WORK_DIR}/logs.db" "${WORK_DIR}/attack.bin" "${WORK_DIR}/calibration.bin" "${WORK_DIR}/stress.bin" "${WORK_DIR}/bifrost.log" "${WORK_DIR}/vegeta-target.json" "${WORK_DIR}/vegeta-target-calibration.json" "${WORK_DIR}/vegeta-target-stress.json" "${WORK_DIR}/vegeta-report.json" "${WORK_DIR}/bifrost-stats.csv" 2>/dev/null || true
|
||||||
|
log_info "Cleanup complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Check for required tools
|
||||||
|
check_dependencies() {
|
||||||
|
log_info "Checking dependencies..."
|
||||||
|
|
||||||
|
if ! command -v go &> /dev/null; then
|
||||||
|
log_error "Go is not installed. Please install Go 1.24.3 or later."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v git &> /dev/null; then
|
||||||
|
log_error "Git is not installed. Please install Git."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "All dependencies found"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Kill any process listening on a specific port (not processes with connections to it)
|
||||||
|
kill_port() {
|
||||||
|
local port=$1
|
||||||
|
local pids=$(lsof -ti "TCP:${port}" -sTCP:LISTEN 2>/dev/null)
|
||||||
|
if [ -n "$pids" ]; then
|
||||||
|
log_warn "Killing existing process(es) listening on port ${port}: ${pids}"
|
||||||
|
echo "$pids" | xargs kill -9 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Kill processes on required ports before starting
|
||||||
|
cleanup_ports() {
|
||||||
|
log_info "Checking for processes on required ports..."
|
||||||
|
kill_port ${MOCKER_PORT}
|
||||||
|
kill_port ${BIFROST_PORT}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install Vegeta if not present
|
||||||
|
install_vegeta() {
|
||||||
|
if ! command -v vegeta &> /dev/null; then
|
||||||
|
log_info "Installing Vegeta load testing tool..."
|
||||||
|
go install github.com/tsenart/vegeta/v12@latest
|
||||||
|
export PATH="$PATH:$(go env GOPATH)/bin"
|
||||||
|
if ! command -v vegeta &> /dev/null; then
|
||||||
|
log_error "Failed to install Vegeta"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_success "Vegeta installed"
|
||||||
|
else
|
||||||
|
log_success "Vegeta already installed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build bifrost-http if binary doesn't exist
|
||||||
|
build_bifrost_http() {
|
||||||
|
if [ -f "${REPO_ROOT}/tmp/bifrost-http" ]; then
|
||||||
|
log_success "bifrost-http binary already exists at ${REPO_ROOT}/tmp/bifrost-http"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Building bifrost-http..."
|
||||||
|
cd "${TRANSPORTS_DIR}"
|
||||||
|
|
||||||
|
if go build -o ${REPO_ROOT}/tmp/bifrost-http .; then
|
||||||
|
log_success "bifrost-http built successfully"
|
||||||
|
else
|
||||||
|
log_error "Failed to build bifrost-http"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "${WORK_DIR}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clone and setup mocker from bifrost-benchmarking
|
||||||
|
setup_mocker() {
|
||||||
|
if [ -d "${REPO_ROOT}/../bifrost-benchmarking" ]; then
|
||||||
|
log_info "Updating bifrost-benchmarking repository..."
|
||||||
|
cd "${REPO_ROOT}/../bifrost-benchmarking"
|
||||||
|
git pull --quiet || true
|
||||||
|
cd "${WORK_DIR}"
|
||||||
|
else
|
||||||
|
log_info "Cloning bifrost-benchmarking repository..."
|
||||||
|
cd "${WORK_DIR}"
|
||||||
|
git clone --depth 1 https://github.com/maximhq/bifrost-benchmarking.git
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Mocker setup complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build mocker binary (avoids go run overhead)
|
||||||
|
build_mocker() {
|
||||||
|
if [ -f "${REPO_ROOT}/tmp/mocker" ]; then
|
||||||
|
log_success "mocker binary already exists at ${REPO_ROOT}/tmp/mocker"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Building mocker..."
|
||||||
|
cd "${MOCKER_DIR}"
|
||||||
|
|
||||||
|
if go build -o "${REPO_ROOT}/tmp/mocker" .; then
|
||||||
|
log_success "mocker built successfully"
|
||||||
|
else
|
||||||
|
log_error "Failed to build mocker"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "${WORK_DIR}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create config.json for bifrost with mocker provider
|
||||||
|
create_config() {
|
||||||
|
log_info "Creating config.json..."
|
||||||
|
|
||||||
|
cat > "${WORK_DIR}/config.json" << 'EOF'
|
||||||
|
{
|
||||||
|
"$schema": "https://www.getbifrost.ai/schema",
|
||||||
|
"client": {
|
||||||
|
"enable_logging": false,
|
||||||
|
"initial_pool_size": 20000,
|
||||||
|
"drop_excess_requests": false,
|
||||||
|
"allow_direct_keys": false
|
||||||
|
},
|
||||||
|
"config_store": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"logs_store": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"providers": {
|
||||||
|
"openai": {
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"name": "mocker-key",
|
||||||
|
"value": "Bearer mocker-key",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"network_config": {
|
||||||
|
"base_url": "http://localhost:8000",
|
||||||
|
"default_request_timeout_in_seconds": 30
|
||||||
|
},
|
||||||
|
"concurrency_and_buffer_size": {
|
||||||
|
"concurrency": 20000,
|
||||||
|
"buffer_size": 40000
|
||||||
|
},
|
||||||
|
"custom_provider_config": {
|
||||||
|
"base_provider_type": "openai",
|
||||||
|
"allowed_requests": {
|
||||||
|
"list_models": false,
|
||||||
|
"chat_completion": true,
|
||||||
|
"chat_completion_stream": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log_success "config.json created"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start mocker with specified latency
|
||||||
|
# Arguments: $1 = latency in ms
|
||||||
|
start_mocker() {
|
||||||
|
local latency_ms=${1:-0}
|
||||||
|
log_info "Starting mocker server on port ${MOCKER_PORT} with ${latency_ms}ms latency..."
|
||||||
|
|
||||||
|
"${REPO_ROOT}/tmp/mocker" -port ${MOCKER_PORT} -host 0.0.0.0 -latency ${latency_ms} &
|
||||||
|
MOCKER_PID=$!
|
||||||
|
|
||||||
|
# Wait for mocker to be ready
|
||||||
|
local max_attempts=30
|
||||||
|
local attempt=0
|
||||||
|
while ! curl -s "http://localhost:${MOCKER_PORT}/v1/chat/completions" -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer mocker-key" \
|
||||||
|
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"test"}]}' > /dev/null 2>&1; do
|
||||||
|
sleep 1
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
if [ $attempt -ge $max_attempts ]; then
|
||||||
|
log_error "Mocker failed to start within ${max_attempts} seconds"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
log_success "Mocker server started (PID: ${MOCKER_PID})"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop mocker
|
||||||
|
stop_mocker() {
|
||||||
|
if [ -n "$MOCKER_PID" ] && kill -0 "$MOCKER_PID" 2>/dev/null; then
|
||||||
|
log_info "Stopping mocker (PID: ${MOCKER_PID})..."
|
||||||
|
kill "$MOCKER_PID" 2>/dev/null || true
|
||||||
|
wait "$MOCKER_PID" 2>/dev/null || true
|
||||||
|
MOCKER_PID=""
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop bifrost-http server
|
||||||
|
stop_bifrost() {
|
||||||
|
if [ -n "$BIFROST_PID" ] && kill -0 "$BIFROST_PID" 2>/dev/null; then
|
||||||
|
log_info "Stopping bifrost (PID: ${BIFROST_PID})..."
|
||||||
|
kill "$BIFROST_PID" 2>/dev/null || true
|
||||||
|
wait "$BIFROST_PID" 2>/dev/null || true
|
||||||
|
BIFROST_PID=""
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start background process stats collection for bifrost
|
||||||
|
# Samples CPU% and RSS every second, writes to CSV
|
||||||
|
start_stats_monitor() {
|
||||||
|
if [ -z "$BIFROST_PID" ] || ! kill -0 "$BIFROST_PID" 2>/dev/null; then
|
||||||
|
log_warn "Cannot start stats monitor: bifrost not running"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "timestamp,cpu_pct,rss_mb" > "${STATS_FILE}"
|
||||||
|
|
||||||
|
(
|
||||||
|
while kill -0 "$BIFROST_PID" 2>/dev/null; do
|
||||||
|
# ps -o %cpu= -o rss= works on both macOS and Linux
|
||||||
|
stats=$(ps -p "$BIFROST_PID" -o %cpu=,rss= 2>/dev/null)
|
||||||
|
if [ -n "$stats" ]; then
|
||||||
|
cpu=$(echo "$stats" | awk '{print $1}')
|
||||||
|
rss_kb=$(echo "$stats" | awk '{print $2}')
|
||||||
|
rss_mb=$(echo "scale=1; ${rss_kb} / 1024" | bc)
|
||||||
|
echo "$(date +%s),${cpu},${rss_mb}" >> "${STATS_FILE}"
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
) &
|
||||||
|
STATS_PID=$!
|
||||||
|
log_info "Stats monitor started (PID: ${STATS_PID})"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop stats monitor and print summary
|
||||||
|
stop_stats_monitor() {
|
||||||
|
if [ -n "$STATS_PID" ] && kill -0 "$STATS_PID" 2>/dev/null; then
|
||||||
|
kill "$STATS_PID" 2>/dev/null || true
|
||||||
|
wait "$STATS_PID" 2>/dev/null || true
|
||||||
|
STATS_PID=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "${STATS_FILE}" ] || [ $(wc -l < "${STATS_FILE}") -le 1 ]; then
|
||||||
|
log_warn "No process stats collected"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compute peak and average CPU/RSS from CSV (skip header)
|
||||||
|
if command -v awk &> /dev/null; then
|
||||||
|
local stats_summary=$(awk -F',' 'NR>1 {
|
||||||
|
cpu_sum+=$2; rss_sum+=$3; n++;
|
||||||
|
if($2>cpu_max) cpu_max=$2;
|
||||||
|
if($3>rss_max) rss_max=$3;
|
||||||
|
} END {
|
||||||
|
if(n>0) printf "%.1f,%.1f,%.1f,%.1f,%d", cpu_sum/n, cpu_max, rss_sum/n, rss_max, n
|
||||||
|
}' "${STATS_FILE}")
|
||||||
|
|
||||||
|
STATS_CPU_AVG=$(echo "$stats_summary" | cut -d',' -f1)
|
||||||
|
STATS_CPU_PEAK=$(echo "$stats_summary" | cut -d',' -f2)
|
||||||
|
STATS_RSS_AVG=$(echo "$stats_summary" | cut -d',' -f3)
|
||||||
|
STATS_RSS_PEAK=$(echo "$stats_summary" | cut -d',' -f4)
|
||||||
|
local samples=$(echo "$stats_summary" | cut -d',' -f5)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_success "Bifrost process stats (single instance, ${samples} samples):"
|
||||||
|
log_info " CPU: avg=${STATS_CPU_AVG}%, peak=${STATS_CPU_PEAK}%"
|
||||||
|
log_info " RSS: avg=${STATS_RSS_AVG}MB, peak=${STATS_RSS_PEAK}MB"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start bifrost-http server
|
||||||
|
start_bifrost() {
|
||||||
|
log_info "Starting bifrost-http on port ${BIFROST_PORT}..."
|
||||||
|
|
||||||
|
cd "${WORK_DIR}"
|
||||||
|
local bifrost_log="${WORK_DIR}/bifrost.log"
|
||||||
|
"${REPO_ROOT}/tmp/bifrost-http" -app-dir "${WORK_DIR}" -port "${BIFROST_PORT}" -host "0.0.0.0" -log-level "info" > "${bifrost_log}" 2>&1 &
|
||||||
|
BIFROST_PID=$!
|
||||||
|
|
||||||
|
# Wait for bifrost to be fully ready (look for "successfully started bifrost" message)
|
||||||
|
local max_attempts=60
|
||||||
|
local attempt=0
|
||||||
|
while ! grep -q "successfully started bifrost" "${bifrost_log}" 2>/dev/null; do
|
||||||
|
sleep 1
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
if [ $attempt -ge $max_attempts ]; then
|
||||||
|
log_error "Bifrost failed to start within ${max_attempts} seconds"
|
||||||
|
log_error "Bifrost log output:"
|
||||||
|
cat "${bifrost_log}" 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Check if process is still running
|
||||||
|
if ! kill -0 "$BIFROST_PID" 2>/dev/null; then
|
||||||
|
log_error "Bifrost process died unexpectedly"
|
||||||
|
log_error "Bifrost log output:"
|
||||||
|
cat "${bifrost_log}" 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
log_success "Bifrost-http started (PID: ${BIFROST_PID})"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract latencies from a vegeta binary results file
|
||||||
|
# Arguments: $1 = path to .bin file
|
||||||
|
# Sets: EXTRACTED_MIN_NS, EXTRACTED_MEAN_NS, EXTRACTED_50_NS, etc.
|
||||||
|
extract_latencies() {
|
||||||
|
local bin_file=$1
|
||||||
|
local json_report_file="${WORK_DIR}/vegeta-report.json"
|
||||||
|
vegeta report -type=json < "${bin_file}" > "${json_report_file}"
|
||||||
|
|
||||||
|
if command -v jq &> /dev/null; then
|
||||||
|
EXTRACTED_MIN_NS=$(jq '.latencies.min // 0' "${json_report_file}")
|
||||||
|
EXTRACTED_MEAN_NS=$(jq '.latencies.mean // 0' "${json_report_file}")
|
||||||
|
EXTRACTED_50_NS=$(jq '.latencies["50th"] // 0' "${json_report_file}")
|
||||||
|
EXTRACTED_90_NS=$(jq '.latencies["90th"] // 0' "${json_report_file}")
|
||||||
|
EXTRACTED_95_NS=$(jq '.latencies["95th"] // 0' "${json_report_file}")
|
||||||
|
EXTRACTED_99_NS=$(jq '.latencies["99th"] // 0' "${json_report_file}")
|
||||||
|
EXTRACTED_MAX_NS=$(jq '.latencies.max // 0' "${json_report_file}")
|
||||||
|
EXTRACTED_SUCCESS=$(jq '.success // 0' "${json_report_file}")
|
||||||
|
EXTRACTED_RATE=$(jq '.rate // 0' "${json_report_file}")
|
||||||
|
EXTRACTED_THROUGHPUT=$(jq '.throughput // 0' "${json_report_file}")
|
||||||
|
elif command -v python3 &> /dev/null; then
|
||||||
|
EXTRACTED_MIN_NS=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('latencies', {}).get('min', 0))")
|
||||||
|
EXTRACTED_MEAN_NS=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('latencies', {}).get('mean', 0))")
|
||||||
|
EXTRACTED_50_NS=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('latencies', {}).get('50th', 0))")
|
||||||
|
EXTRACTED_90_NS=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('latencies', {}).get('90th', 0))")
|
||||||
|
EXTRACTED_95_NS=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('latencies', {}).get('95th', 0))")
|
||||||
|
EXTRACTED_99_NS=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('latencies', {}).get('99th', 0))")
|
||||||
|
EXTRACTED_MAX_NS=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('latencies', {}).get('max', 0))")
|
||||||
|
EXTRACTED_SUCCESS=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('success', 0))")
|
||||||
|
EXTRACTED_RATE=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('rate', 0))")
|
||||||
|
EXTRACTED_THROUGHPUT=$(python3 -c "import json; d=json.load(open('${json_report_file}')); print(d.get('throughput', 0))")
|
||||||
|
else
|
||||||
|
log_error "Neither jq nor python3 found. Cannot parse JSON results."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "${json_report_file}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Phase 1: Overhead measurement (mocker at ${OVERHEAD_MOCKER_LATENCY_MS}ms)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# Calibration: Vegeta -> Mocker direct (with latency)
|
||||||
|
# Measures: Vegeta HTTP client + localhost network round-trip + mocker response generation
|
||||||
|
run_calibration() {
|
||||||
|
echo ""
|
||||||
|
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||||
|
echo "║ Calibration: Vegeta -> Mocker (${OVERHEAD_MOCKER_LATENCY_MS}ms, direct) ║"
|
||||||
|
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
log_info "Measuring Vegeta + network baseline (mocker at ${OVERHEAD_MOCKER_LATENCY_MS}ms latency)"
|
||||||
|
log_info "Duration: ${OVERHEAD_DURATION}s at ${RATE} RPS, ~$(( RATE * OVERHEAD_MOCKER_LATENCY_MS / 1000 )) concurrent"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
local target_file="${WORK_DIR}/vegeta-target-calibration.json"
|
||||||
|
local payload='{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Hello, how are you?"}]}'
|
||||||
|
|
||||||
|
cat > "${target_file}" << EOF
|
||||||
|
{"method": "POST", "url": "http://localhost:${MOCKER_PORT}/v1/chat/completions", "header": {"Content-Type": ["application/json"], "Authorization": ["Bearer mocker-key"]}, "body": "$(echo -n "${payload}" | base64)"}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
vegeta attack \
|
||||||
|
-format=json \
|
||||||
|
-targets="${target_file}" \
|
||||||
|
-rate="${RATE}" \
|
||||||
|
-duration="${OVERHEAD_DURATION}s" \
|
||||||
|
-timeout="$((OVERHEAD_MOCKER_LATENCY_MS / 1000 + 5))s" \
|
||||||
|
-workers=$((RATE * OVERHEAD_MOCKER_LATENCY_MS / 1000)) \
|
||||||
|
-max-workers="${MAX_WORKERS}" > "${WORK_DIR}/calibration.bin"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "Calibration complete. Results:"
|
||||||
|
vegeta report < "${WORK_DIR}/calibration.bin"
|
||||||
|
|
||||||
|
extract_latencies "${WORK_DIR}/calibration.bin"
|
||||||
|
|
||||||
|
log_info "Actual RPS: $(printf "%.0f" $EXTRACTED_RATE) (configured: ${RATE})"
|
||||||
|
|
||||||
|
CAL_MIN_NS=$EXTRACTED_MIN_NS
|
||||||
|
CAL_MEAN_NS=$EXTRACTED_MEAN_NS
|
||||||
|
CAL_50_NS=$EXTRACTED_50_NS
|
||||||
|
CAL_90_NS=$EXTRACTED_90_NS
|
||||||
|
CAL_95_NS=$EXTRACTED_95_NS
|
||||||
|
CAL_99_NS=$EXTRACTED_99_NS
|
||||||
|
CAL_MAX_NS=$EXTRACTED_MAX_NS
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_success "Calibration baseline (per bucket):"
|
||||||
|
log_info " Min: $(echo "scale=2; $CAL_MIN_NS / 1000" | bc)µs"
|
||||||
|
log_info " Mean: $(echo "scale=2; $CAL_MEAN_NS / 1000" | bc)µs"
|
||||||
|
log_info " P50: $(echo "scale=2; $CAL_50_NS / 1000" | bc)µs"
|
||||||
|
log_info " P90: $(echo "scale=2; $CAL_90_NS / 1000" | bc)µs"
|
||||||
|
log_info " P95: $(echo "scale=2; $CAL_95_NS / 1000" | bc)µs"
|
||||||
|
log_info " P99: $(echo "scale=2; $CAL_99_NS / 1000" | bc)µs"
|
||||||
|
log_info " Max: $(echo "scale=2; $CAL_MAX_NS / 1000" | bc)µs"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Overhead test: Vegeta -> Bifrost -> Mocker (with latency)
|
||||||
|
# Same duration/rate as calibration so percentile distributions are comparable
|
||||||
|
run_overhead_test() {
|
||||||
|
echo ""
|
||||||
|
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||||
|
echo "║ Overhead Test: Vegeta -> Bifrost -> Mocker (${OVERHEAD_MOCKER_LATENCY_MS}ms) ║"
|
||||||
|
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
log_info "Measuring Bifrost overhead (single instance, mocker at ${OVERHEAD_MOCKER_LATENCY_MS}ms latency)"
|
||||||
|
log_info "Duration: ${OVERHEAD_DURATION}s at ${RATE} RPS, ~$(( RATE * OVERHEAD_MOCKER_LATENCY_MS / 1000 )) concurrent requests through Bifrost"
|
||||||
|
log_info "Overhead consists of: vegetta overhead and mocker timeout jitter"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
local target_file="${WORK_DIR}/vegeta-target.json"
|
||||||
|
local payload='{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"Hello, how are you?"}]}'
|
||||||
|
|
||||||
|
cat > "${target_file}" << EOF
|
||||||
|
{"method": "POST", "url": "http://localhost:${BIFROST_PORT}/v1/chat/completions", "header": {"Content-Type": ["application/json"]}, "body": "$(echo -n "${payload}" | base64)"}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
vegeta attack \
|
||||||
|
-format=json \
|
||||||
|
-targets="${target_file}" \
|
||||||
|
-rate="${RATE}" \
|
||||||
|
-duration="${OVERHEAD_DURATION}s" \
|
||||||
|
-timeout="$((OVERHEAD_MOCKER_LATENCY_MS / 1000 + 5))s" \
|
||||||
|
-workers=$((RATE * OVERHEAD_MOCKER_LATENCY_MS / 1000)) \
|
||||||
|
-max-workers="${MAX_WORKERS}" > "${WORK_DIR}/attack.bin"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "Overhead test complete. Results:"
|
||||||
|
vegeta report < "${WORK_DIR}/attack.bin"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "Latency histogram:"
|
||||||
|
vegeta report -type=hist[0,100us,500us,1ms,5ms,10ms,50ms,100ms] < "${WORK_DIR}/attack.bin" || log_warn "Histogram generation failed"
|
||||||
|
|
||||||
|
# Extract and compute overhead
|
||||||
|
extract_latencies "${WORK_DIR}/attack.bin"
|
||||||
|
|
||||||
|
log_info " Raw latencies (ns): min=$EXTRACTED_MIN_NS, mean=$EXTRACTED_MEAN_NS, p50=$EXTRACTED_50_NS, p99=$EXTRACTED_99_NS, max=$EXTRACTED_MAX_NS"
|
||||||
|
log_info " Success rate: $EXTRACTED_SUCCESS"
|
||||||
|
log_info " Actual RPS: $(printf "%.0f" $EXTRACTED_RATE) (configured: ${RATE})"
|
||||||
|
|
||||||
|
if [ -z "$EXTRACTED_MIN_NS" ] || [ "$EXTRACTED_MIN_NS" = "0" ] || [ "$EXTRACTED_MIN_NS" = "null" ]; then
|
||||||
|
log_error "Failed to extract latency values from vegeta report"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Subtract calibration per bucket: overhead = through_bifrost - direct_to_mocker
|
||||||
|
local us_min=$(printf "%.2f" $(echo "scale=4; ($EXTRACTED_MIN_NS - $CAL_MIN_NS) / 1000" | bc))
|
||||||
|
local us_mean=$(printf "%.2f" $(echo "scale=4; ($EXTRACTED_MEAN_NS - $CAL_MEAN_NS) / 1000" | bc))
|
||||||
|
local us_50=$(printf "%.2f" $(echo "scale=4; ($EXTRACTED_50_NS - $CAL_50_NS) / 1000" | bc))
|
||||||
|
local us_90=$(printf "%.2f" $(echo "scale=4; ($EXTRACTED_90_NS - $CAL_90_NS) / 1000" | bc))
|
||||||
|
local us_95=$(printf "%.2f" $(echo "scale=4; ($EXTRACTED_95_NS - $CAL_95_NS) / 1000" | bc))
|
||||||
|
local us_99=$(printf "%.2f" $(echo "scale=4; ($EXTRACTED_99_NS - $CAL_99_NS) / 1000" | bc))
|
||||||
|
local us_max=$(printf "%.2f" $(echo "scale=4; ($EXTRACTED_MAX_NS - $CAL_MAX_NS) / 1000" | bc))
|
||||||
|
|
||||||
|
local success_pct=$(printf "%.2f" $(echo "scale=4; $EXTRACTED_SUCCESS * 100" | bc))
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_success "Bifrost overhead (per bucket):"
|
||||||
|
log_info " Min: ${us_min}µs"
|
||||||
|
log_info " Mean: ${us_mean}µs"
|
||||||
|
log_info " P50: ${us_50}µs"
|
||||||
|
log_info " P90: ${us_90}µs"
|
||||||
|
log_info " P95: ${us_95}µs"
|
||||||
|
log_info " P99: ${us_99}µs"
|
||||||
|
log_info " Max: ${us_max}µs"
|
||||||
|
|
||||||
|
local actual_rps=$(printf "%.0f" $EXTRACTED_RATE)
|
||||||
|
|
||||||
|
# Write results
|
||||||
|
cat > "${RESULTS_FILE}" << EOF
|
||||||
|
# Bifrost Load Test Results (single instance, ${actual_rps} RPS)
|
||||||
|
|
||||||
|
## Bifrost Processing Overhead
|
||||||
|
|
||||||
|
| Metric | Actual RPS | Duration | Concurrent | Success Rate | Min | Mean | P50 | P90 | P95 | P99 | Max |
|
||||||
|
|--------|-----------|----------|------------|--------------|-----|------|-----|-----|-----|-----|-----|
|
||||||
|
| Overhead | ${actual_rps} | ${OVERHEAD_DURATION}s | ~$((RATE * OVERHEAD_MOCKER_LATENCY_MS / 1000)) | ${success_pct}% | ${us_min}µs | ${us_mean}µs | ${us_50}µs | ${us_90}µs | ${us_95}µs | ${us_99}µs | ${us_max}µs |
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo '{"overhead": {"configured_rate": '"${RATE}"', "actual_rate": '"${actual_rps}"', "duration": '"${OVERHEAD_DURATION}"', "concurrent": '$((RATE * OVERHEAD_MOCKER_LATENCY_MS / 1000))', "success_rate": '"${success_pct}"', "latency_us": {"min": '"${us_min}"', "mean": '"${us_mean}"', "p50": '"${us_50}"', "p90": '"${us_90}"', "p95": '"${us_95}"', "p99": '"${us_99}"', "max": '"${us_max}"'}}, "timestamp": "'"$(date -u +"%Y-%m-%dT%H:%M:%SZ")"'"}' > "${RESULTS_JSON}"
|
||||||
|
|
||||||
|
# Check tiered thresholds (skip Min/Max — single-point extremes are too noisy)
|
||||||
|
local failed=0
|
||||||
|
local labels=("Mean" "P50" "P90" "P95" "P99")
|
||||||
|
local real_values=($EXTRACTED_MEAN_NS $EXTRACTED_50_NS $EXTRACTED_90_NS $EXTRACTED_95_NS $EXTRACTED_99_NS)
|
||||||
|
local cal_values=($CAL_MEAN_NS $CAL_50_NS $CAL_90_NS $CAL_95_NS $CAL_99_NS)
|
||||||
|
local thresholds=($MAX_OVERHEAD_MEAN_US $MAX_OVERHEAD_P50_US $MAX_OVERHEAD_P90_US $MAX_OVERHEAD_P95_US $MAX_OVERHEAD_P99_US)
|
||||||
|
local extras=()
|
||||||
|
|
||||||
|
for i in "${!real_values[@]}"; do
|
||||||
|
local overhead_us=$(( (real_values[i] - cal_values[i]) / 1000 ))
|
||||||
|
if [ "$overhead_us" -gt "${thresholds[i]}" ]; then
|
||||||
|
extras+=("${labels[i]}:${overhead_us}:${thresholds[i]}")
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$failed" -eq 1 ]; then
|
||||||
|
echo ""
|
||||||
|
log_error "FAILED: Bifrost overhead exceeded tiered thresholds"
|
||||||
|
log_error "Overhead consists of: vegetta overhead and mocker timeout jitter. In real-world the P99 overhead will be approximately 100 microseconds."
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}| Bucket | Overhead (µs) | Threshold (µs) |${NC}"
|
||||||
|
echo -e "${RED}|--------|---------------|----------------|${NC}"
|
||||||
|
for entry in "${extras[@]}"; do
|
||||||
|
IFS=: read -r bucket overhead threshold <<< "$entry"
|
||||||
|
echo -e "${RED}| ${bucket} | ${overhead}µs | ${threshold}µs |${NC}"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
stop_stats_monitor
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "All overhead buckets within tiered thresholds (mean<${MAX_OVERHEAD_MEAN_US}µs, p50<${MAX_OVERHEAD_P50_US}µs, p90<${MAX_OVERHEAD_P90_US}µs, p95<${MAX_OVERHEAD_P95_US}µs, p99<${MAX_OVERHEAD_P99_US}µs)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Phase 2: Stress test (mocker at 10s latency)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# Arguments: $1 = label (e.g. "Stress #1", "Stress #2")
|
||||||
|
run_stress_test() {
|
||||||
|
local label="${1:-Stress}"
|
||||||
|
local bin_file="${WORK_DIR}/stress.bin"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||||
|
echo "║ ${label}: ${RATE} RPS with ${STRESS_MOCKER_LATENCY_MS}ms mocker latency ║"
|
||||||
|
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
log_info "Testing single Bifrost instance under sustained concurrency"
|
||||||
|
log_info "Duration: ${STRESS_DURATION}s at ${RATE} RPS (${STRESS_MOCKER_LATENCY_MS}ms mocker latency)"
|
||||||
|
log_info "Expected concurrent requests: ~$(( RATE * STRESS_MOCKER_LATENCY_MS / 1000 )) (provider concurrency: 15,000, buffer: 20,000)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
local target_file="${WORK_DIR}/vegeta-target-stress.json"
|
||||||
|
local payload='{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"Hello, how are you?"}]}'
|
||||||
|
|
||||||
|
cat > "${target_file}" << EOF
|
||||||
|
{"method": "POST", "url": "http://localhost:${BIFROST_PORT}/v1/chat/completions", "header": {"Content-Type": ["application/json"]}, "body": "$(echo -n "${payload}" | base64)"}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
vegeta attack \
|
||||||
|
-format=json \
|
||||||
|
-targets="${target_file}" \
|
||||||
|
-rate="${RATE}" \
|
||||||
|
-duration="${STRESS_DURATION}s" \
|
||||||
|
-timeout="30s" \
|
||||||
|
-workers=$((RATE * STRESS_MOCKER_LATENCY_MS / 1000)) \
|
||||||
|
-max-workers="${MAX_WORKERS}" > "${bin_file}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "${label} complete. Results:"
|
||||||
|
vegeta report < "${bin_file}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "Latency histogram:"
|
||||||
|
vegeta report -type=hist[0,1ms,5ms,10ms,50ms,100ms,500ms,1s,5s,10s,15s] < "${bin_file}" || log_warn "Histogram generation failed"
|
||||||
|
|
||||||
|
# Check success rate
|
||||||
|
extract_latencies "${bin_file}"
|
||||||
|
|
||||||
|
local success_pct=$(printf "%.2f" $(echo "scale=4; $EXTRACTED_SUCCESS * 100" | bc))
|
||||||
|
|
||||||
|
log_info "Actual RPS: $(printf "%.0f" $EXTRACTED_RATE) (configured: ${RATE})"
|
||||||
|
|
||||||
|
local stress_actual_rps=$(printf "%.0f" $EXTRACTED_RATE)
|
||||||
|
|
||||||
|
# Append stress test results to results file
|
||||||
|
cat >> "${RESULTS_FILE}" << EOF
|
||||||
|
|
||||||
|
## ${label} (${STRESS_MOCKER_LATENCY_MS}ms mocker latency)
|
||||||
|
|
||||||
|
| Metric | Actual RPS | Duration | Concurrent | Success Rate | Min | Mean | P50 | P90 | P95 | P99 | Max |
|
||||||
|
|--------|-----------|----------|------------|--------------|-----|------|-----|-----|-----|-----|-----|
|
||||||
|
| ${label} | ${stress_actual_rps} | ${STRESS_DURATION}s | ~$((RATE * STRESS_MOCKER_LATENCY_MS / 1000)) | ${success_pct}% | $(echo "scale=2; $EXTRACTED_MIN_NS / 1000000" | bc)ms | $(echo "scale=2; $EXTRACTED_MEAN_NS / 1000000" | bc)ms | $(echo "scale=2; $EXTRACTED_50_NS / 1000000" | bc)ms | $(echo "scale=2; $EXTRACTED_90_NS / 1000000" | bc)ms | $(echo "scale=2; $EXTRACTED_95_NS / 1000000" | bc)ms | $(echo "scale=2; $EXTRACTED_99_NS / 1000000" | bc)ms | $(echo "scale=2; $EXTRACTED_MAX_NS / 1000000" | bc)ms |
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ "$success_pct" != "100.00" ]; then
|
||||||
|
echo ""
|
||||||
|
log_error "FAILED: ${label} success rate is ${success_pct}% (expected 100%)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "${label} passed: ${success_pct}% success rate"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Finalize
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
finalize_results() {
|
||||||
|
# Append process stats if available
|
||||||
|
local has_overhead_stats=false
|
||||||
|
local has_stress_stats=false
|
||||||
|
|
||||||
|
if [ -n "$OVERHEAD_STATS_CPU_PEAK" ]; then
|
||||||
|
has_overhead_stats=true
|
||||||
|
fi
|
||||||
|
if [ -n "$STATS_CPU_PEAK" ]; then
|
||||||
|
has_stress_stats=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$has_overhead_stats" = true ] || [ "$has_stress_stats" = true ]; then
|
||||||
|
cat >> "${RESULTS_FILE}" << 'EOF'
|
||||||
|
|
||||||
|
## Bifrost Process Stats (single instance)
|
||||||
|
|
||||||
|
| Phase | CPU Avg | CPU Peak | RSS Avg | RSS Peak |
|
||||||
|
|-------|---------|----------|---------|----------|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ "$has_overhead_stats" = true ]; then
|
||||||
|
echo "| Overhead | ${OVERHEAD_STATS_CPU_AVG}% | ${OVERHEAD_STATS_CPU_PEAK}% | ${OVERHEAD_STATS_RSS_AVG}MB | ${OVERHEAD_STATS_RSS_PEAK}MB |" >> "${RESULTS_FILE}"
|
||||||
|
fi
|
||||||
|
if [ "$has_stress_stats" = true ]; then
|
||||||
|
echo "| Stress | ${STATS_CPU_AVG}% | ${STATS_CPU_PEAK}% | ${STATS_RSS_AVG}MB | ${STATS_RSS_PEAK}MB |" >> "${RESULTS_FILE}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >> "${RESULTS_FILE}" << EOF
|
||||||
|
|
||||||
|
## Method
|
||||||
|
|
||||||
|
- **Single instance**: All tests run against one bifrost-http process at ${RATE} RPS
|
||||||
|
- **Overhead measurement**: Mocker at ${OVERHEAD_MOCKER_LATENCY_MS}ms latency, calibration (Vegeta->Mocker) subtracted from test (Vegeta->Bifrost->Mocker)
|
||||||
|
- **Stress test**: Mocker at ${STRESS_MOCKER_LATENCY_MS}ms latency, verifies 100% success under sustained concurrency
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Overhead values are in microseconds (µs), stress test values in milliseconds (ms)
|
||||||
|
- Overhead ignores the mocker jitter, local network request queuing. In real-world the P99 overhead will be approximately 100 microseconds.
|
||||||
|
- Tiered overhead thresholds: mean<${MAX_OVERHEAD_MEAN_US}µs, p50<${MAX_OVERHEAD_P50_US}µs, p90<${MAX_OVERHEAD_P90_US}µs, p95<${MAX_OVERHEAD_P95_US}µs, p99<${MAX_OVERHEAD_P99_US}µs
|
||||||
|
- P50/P90/P95/P99 represent percentile latencies
|
||||||
|
|
||||||
|
---
|
||||||
|
*Generated by Bifrost Load Test Script*
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Update JSON with stress results and process stats
|
||||||
|
local tmp_json=$(mktemp)
|
||||||
|
if command -v jq &> /dev/null; then
|
||||||
|
jq --arg sr "$(printf "%.2f" $(echo "scale=4; $EXTRACTED_SUCCESS * 100" | bc))" \
|
||||||
|
--arg cpu_avg "${STATS_CPU_AVG:-0}" --arg cpu_peak "${STATS_CPU_PEAK:-0}" \
|
||||||
|
--arg rss_avg "${STATS_RSS_AVG:-0}" --arg rss_peak "${STATS_RSS_PEAK:-0}" \
|
||||||
|
--arg oh_cpu_avg "${OVERHEAD_STATS_CPU_AVG:-0}" --arg oh_cpu_peak "${OVERHEAD_STATS_CPU_PEAK:-0}" \
|
||||||
|
--arg oh_rss_avg "${OVERHEAD_STATS_RSS_AVG:-0}" --arg oh_rss_peak "${OVERHEAD_STATS_RSS_PEAK:-0}" \
|
||||||
|
'.stress = {"rate": '"${RATE}"', "duration": '"${STRESS_DURATION}"', "mocker_latency_ms": '"${STRESS_MOCKER_LATENCY_MS}"', "success_rate": ($sr | tonumber)} | .process_stats = {"overhead": {"cpu_avg_pct": ($oh_cpu_avg | tonumber), "cpu_peak_pct": ($oh_cpu_peak | tonumber), "rss_avg_mb": ($oh_rss_avg | tonumber), "rss_peak_mb": ($oh_rss_peak | tonumber)}, "stress": {"cpu_avg_pct": ($cpu_avg | tonumber), "cpu_peak_pct": ($cpu_peak | tonumber), "rss_avg_mb": ($rss_avg | tonumber), "rss_peak_mb": ($rss_peak | tonumber)}}' \
|
||||||
|
"${RESULTS_JSON}" > "${tmp_json}"
|
||||||
|
mv "${tmp_json}" "${RESULTS_JSON}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Results saved to:"
|
||||||
|
log_info " - Markdown: ${RESULTS_FILE}"
|
||||||
|
log_info " - JSON: ${RESULTS_JSON}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main execution
|
||||||
|
main() {
|
||||||
|
echo ""
|
||||||
|
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||||
|
echo "║ Bifrost Load Test (single instance, ${RATE} RPS) ║"
|
||||||
|
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
log_info "Configuration: single bifrost-http instance, ${RATE} RPS"
|
||||||
|
log_info "Provider concurrency: 15,000 (buffer: 20,000)"
|
||||||
|
log_info "Overhead thresholds: mean<${MAX_OVERHEAD_MEAN_US}µs, p50<${MAX_OVERHEAD_P50_US}µs, p90<${MAX_OVERHEAD_P90_US}µs, p95<${MAX_OVERHEAD_P95_US}µs, p99<${MAX_OVERHEAD_P99_US}µs"
|
||||||
|
log_info "Phase 1: Overhead measurement — ${OVERHEAD_MOCKER_LATENCY_MS}ms mocker, ${OVERHEAD_DURATION}s, ~$(( RATE * OVERHEAD_MOCKER_LATENCY_MS / 1000 )) concurrent requests"
|
||||||
|
log_info "Phase 2: Stress test — ${STRESS_MOCKER_LATENCY_MS}ms mocker, ${STRESS_DURATION}s, ~$(( RATE * STRESS_MOCKER_LATENCY_MS / 1000 )) concurrent requests"
|
||||||
|
|
||||||
|
check_dependencies
|
||||||
|
install_vegeta
|
||||||
|
build_bifrost_http
|
||||||
|
setup_mocker
|
||||||
|
build_mocker
|
||||||
|
create_config
|
||||||
|
cleanup_ports
|
||||||
|
|
||||||
|
# ── Phase 1: Overhead measurement with ${OVERHEAD_MOCKER_LATENCY_MS}ms mocker ──
|
||||||
|
start_mocker ${OVERHEAD_MOCKER_LATENCY_MS}
|
||||||
|
start_bifrost
|
||||||
|
start_stats_monitor
|
||||||
|
|
||||||
|
run_calibration
|
||||||
|
run_overhead_test
|
||||||
|
|
||||||
|
# ── Collect process stats from overhead phase ──
|
||||||
|
stop_stats_monitor
|
||||||
|
OVERHEAD_STATS_CPU_AVG="${STATS_CPU_AVG}"
|
||||||
|
OVERHEAD_STATS_CPU_PEAK="${STATS_CPU_PEAK}"
|
||||||
|
OVERHEAD_STATS_RSS_AVG="${STATS_RSS_AVG}"
|
||||||
|
OVERHEAD_STATS_RSS_PEAK="${STATS_RSS_PEAK}"
|
||||||
|
|
||||||
|
# ── Phase 2: Stress test with high-latency mocker ──
|
||||||
|
# Restart both mocker and bifrost to ensure a clean fasthttp connection pool.
|
||||||
|
# Without restarting bifrost, stale TCP connections from the overhead phase
|
||||||
|
# (which used a different mocker process) cause immediate 400s on POST requests
|
||||||
|
# because fasthttp does not retry non-idempotent methods on broken connections.
|
||||||
|
stop_mocker
|
||||||
|
stop_bifrost
|
||||||
|
start_mocker ${STRESS_MOCKER_LATENCY_MS}
|
||||||
|
start_bifrost
|
||||||
|
start_stats_monitor
|
||||||
|
|
||||||
|
run_stress_test "Stress #1"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "Waiting 30s before second stress test (idle period)..."
|
||||||
|
sleep 30
|
||||||
|
|
||||||
|
run_stress_test "Stress #2"
|
||||||
|
|
||||||
|
# ── Collect process stats from stress phase ──
|
||||||
|
stop_stats_monitor
|
||||||
|
|
||||||
|
# ── Finalize ──
|
||||||
|
finalize_results
|
||||||
|
|
||||||
|
cleanup_ports
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Print final summary
|
||||||
|
echo "╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗"
|
||||||
|
echo "║ FINAL RESULTS SUMMARY ║"
|
||||||
|
echo "╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
cat "${RESULTS_FILE}"
|
||||||
|
echo ""
|
||||||
|
log_success "All tests passed!"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
250
.github/workflows/scripts/push-cli-mintlify-changelog.sh
vendored
Executable file
250
.github/workflows/scripts/push-cli-mintlify-changelog.sh
vendored
Executable file
@@ -0,0 +1,250 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
VERSION=$1
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo "Usage: $0 <version>"
|
||||||
|
echo "Example: $0 0.10.0"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION_WITH_PREFIX="cli-v$VERSION"
|
||||||
|
|
||||||
|
# Check if this page already exists in docs/changelogs/
|
||||||
|
if [ -f "docs/changelogs/$VERSION_WITH_PREFIX.mdx" ]; then
|
||||||
|
echo "✅ Changelog for $VERSION_WITH_PREFIX already exists"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Source changelog utilities
|
||||||
|
source "$(dirname "$0")/changelog-utils.sh"
|
||||||
|
|
||||||
|
# Get current date
|
||||||
|
CURRENT_DATE=$(date +"%Y-%m-%d")
|
||||||
|
|
||||||
|
# Get changelog content from cli/changelog.md
|
||||||
|
CLI_CHANGELOG_PATH="cli/changelog.md"
|
||||||
|
if [ ! -f "$CLI_CHANGELOG_PATH" ]; then
|
||||||
|
echo "❌ CLI changelog not found at $CLI_CHANGELOG_PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CHANGELOG_CONTENT=$(get_file_content "$CLI_CHANGELOG_PATH")
|
||||||
|
if [ -z "$CHANGELOG_CONTENT" ]; then
|
||||||
|
echo "❌ CLI changelog is empty"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Preparing changelog file
|
||||||
|
CHANGELOG_BODY="---
|
||||||
|
title: \"v$VERSION\"
|
||||||
|
description: \"v$VERSION changelog - $CURRENT_DATE\"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Update label=\"Bifrost CLI\" description=\"v$VERSION\">
|
||||||
|
$CHANGELOG_CONTENT
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
"
|
||||||
|
|
||||||
|
# Write to file
|
||||||
|
mkdir -p docs/changelogs
|
||||||
|
echo "$CHANGELOG_BODY" > "docs/changelogs/$VERSION_WITH_PREFIX.mdx"
|
||||||
|
echo "✅ Created docs/changelogs/$VERSION_WITH_PREFIX.mdx"
|
||||||
|
|
||||||
|
# Clear the CLI changelog file after processing
|
||||||
|
printf '' > "$CLI_CHANGELOG_PATH"
|
||||||
|
echo "✅ Cleared $CLI_CHANGELOG_PATH"
|
||||||
|
|
||||||
|
# Update docs.json to include this new changelog route in the Bifrost CLI menu
|
||||||
|
route="changelogs/$VERSION_WITH_PREFIX"
|
||||||
|
if ! grep -q "\"$route\"" docs/docs.json; then
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const docs = JSON.parse(fs.readFileSync('docs/docs.json', 'utf8'));
|
||||||
|
|
||||||
|
// Semantic version comparison function
|
||||||
|
// Extracts version from route/filename and compares in descending order (newest first)
|
||||||
|
function compareVersionsDesc(a, b) {
|
||||||
|
// Extract route string from string or object
|
||||||
|
const routeA = typeof a === 'string' ? a : '';
|
||||||
|
const routeB = typeof b === 'string' ? b : '';
|
||||||
|
|
||||||
|
// Extract version from route (e.g., 'changelogs/cli-v0.10.0' -> 'cli-v0.10.0')
|
||||||
|
const versionA = routeA.split('/').pop() || '';
|
||||||
|
const versionB = routeB.split('/').pop() || '';
|
||||||
|
|
||||||
|
// Remove 'cli-v' or 'v' prefix and split into parts
|
||||||
|
const partsA = versionA.replace(/^(cli-)?v/, '').split(/[.-]/).map(p => {
|
||||||
|
const num = parseInt(p, 10);
|
||||||
|
return isNaN(num) ? p : num;
|
||||||
|
});
|
||||||
|
const partsB = versionB.replace(/^(cli-)?v/, '').split(/[.-]/).map(p => {
|
||||||
|
const num = parseInt(p, 10);
|
||||||
|
return isNaN(num) ? p : num;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compare each part (major, minor, patch, pre-release, etc.)
|
||||||
|
const maxLength = Math.max(partsA.length, partsB.length);
|
||||||
|
for (let i = 0; i < maxLength; i++) {
|
||||||
|
// Release vs prerelease: release is newer (no suffix > has suffix)
|
||||||
|
if (partsA[i] === undefined && partsB[i] !== undefined) {
|
||||||
|
return -1; // A (release) comes first in descending order
|
||||||
|
}
|
||||||
|
if (partsB[i] === undefined && partsA[i] !== undefined) {
|
||||||
|
return 1; // B (release) comes first in descending order
|
||||||
|
}
|
||||||
|
|
||||||
|
const partA = partsA[i];
|
||||||
|
const partB = partsB[i];
|
||||||
|
|
||||||
|
// If both are numbers, compare numerically
|
||||||
|
if (typeof partA === 'number' && typeof partB === 'number') {
|
||||||
|
if (partA !== partB) {
|
||||||
|
return partB - partA; // Descending order
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle prerelease strings with numeric suffixes (e.g., 'prerelease10')
|
||||||
|
const strA = String(partA);
|
||||||
|
const strB = String(partB);
|
||||||
|
const matchA = strA.match(/^([a-zA-Z]+)(\\d+)$/);
|
||||||
|
const matchB = strB.match(/^([a-zA-Z]+)(\\d+)$/);
|
||||||
|
|
||||||
|
if (matchA && matchB && matchA[1] === matchB[1]) {
|
||||||
|
// Same prefix, compare numbers numerically
|
||||||
|
const numA = parseInt(matchA[2], 10);
|
||||||
|
const numB = parseInt(matchB[2], 10);
|
||||||
|
if (numA !== numB) {
|
||||||
|
return numB - numA; // Descending order
|
||||||
|
}
|
||||||
|
} else if (strA !== strB) {
|
||||||
|
return strB.localeCompare(strA); // Descending order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0; // Equal
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort a pages array by semver (descending)
|
||||||
|
function sortPagesBySemver(pages) {
|
||||||
|
return pages.slice().sort(compareVersionsDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current month/year
|
||||||
|
const releaseDate = new Date('$CURRENT_DATE');
|
||||||
|
const currentDate = new Date();
|
||||||
|
const releaseMonthYear = releaseDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
|
||||||
|
const currentMonthYear = currentDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
|
||||||
|
|
||||||
|
// Find the Changelogs tab
|
||||||
|
const changelogsTab = docs.navigation.tabs.find(tab => tab.tab === 'Changelogs');
|
||||||
|
if (!changelogsTab) {
|
||||||
|
console.error('Changelogs tab not found');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the Bifrost CLI menu item
|
||||||
|
const cliMenuItem = changelogsTab.menu.find(item => item.item === 'Bifrost CLI');
|
||||||
|
if (!cliMenuItem) {
|
||||||
|
console.error('Bifrost CLI menu item not found');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all top-level entries and existing groups
|
||||||
|
const topLevelEntries = cliMenuItem.pages.filter(p => typeof p === 'string');
|
||||||
|
const existingGroups = cliMenuItem.pages.filter(p => typeof p === 'object');
|
||||||
|
|
||||||
|
// Check if we need to group existing top-level entries
|
||||||
|
if (topLevelEntries.length > 0) {
|
||||||
|
// Get the month of the first top-level entry (they should all be from same month)
|
||||||
|
const firstEntryPath = topLevelEntries[0].replace('changelogs/', '') + '.mdx';
|
||||||
|
const firstEntryFile = 'docs/changelogs/' + firstEntryPath;
|
||||||
|
|
||||||
|
let topLevelMonth = null;
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(firstEntryFile, 'utf8');
|
||||||
|
const descMatch = content.match(/description:\\s*\"[^\"]*?(\\d{4}-\\d{2}-\\d{2})[^\"]*\"/);
|
||||||
|
if (descMatch) {
|
||||||
|
const entryDate = new Date(descMatch[1]);
|
||||||
|
topLevelMonth = entryDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(\`Warning: Could not read entry file \${firstEntryFile}: \${e.message}\`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only group if the month has changed
|
||||||
|
if (topLevelMonth && topLevelMonth !== releaseMonthYear) {
|
||||||
|
console.log(\`📦 Month changed from \${topLevelMonth} to \${releaseMonthYear}\`);
|
||||||
|
console.log(\`📦 Grouping \${topLevelEntries.length} top-level entries into \${topLevelMonth} group...\`);
|
||||||
|
|
||||||
|
// Create a group for all existing top-level entries
|
||||||
|
const previousMonthGroup = {
|
||||||
|
group: topLevelMonth,
|
||||||
|
pages: sortPagesBySemver(topLevelEntries)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add this group at the top of existing groups
|
||||||
|
existingGroups.unshift(previousMonthGroup);
|
||||||
|
console.log(\`✅ Created \${topLevelMonth} group with \${topLevelEntries.length} entries (sorted)\`);
|
||||||
|
|
||||||
|
// Clear top-level entries (they're now in the group)
|
||||||
|
cliMenuItem.pages = existingGroups;
|
||||||
|
} else {
|
||||||
|
console.log(\`📋 Same month (\${releaseMonthYear}), keeping existing top-level entries\`);
|
||||||
|
// Keep existing structure (top-level entries + groups)
|
||||||
|
cliMenuItem.pages = [...topLevelEntries, ...existingGroups];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRoute = '$route';
|
||||||
|
|
||||||
|
// Add the new changelog at the top level
|
||||||
|
cliMenuItem.pages.unshift(newRoute);
|
||||||
|
console.log(\`✅ Added \${newRoute} to top level\`);
|
||||||
|
|
||||||
|
// Sort the top-level pages array by semver
|
||||||
|
const topLevelPages = cliMenuItem.pages.filter(p => typeof p === 'string');
|
||||||
|
const groupPages = cliMenuItem.pages.filter(p => typeof p === 'object');
|
||||||
|
|
||||||
|
if (topLevelPages.length > 0) {
|
||||||
|
const sortedTopLevel = sortPagesBySemver(topLevelPages);
|
||||||
|
cliMenuItem.pages = [...sortedTopLevel, ...groupPages];
|
||||||
|
console.log(\`✅ Sorted \${topLevelPages.length} top-level pages by semver\`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort each group's pages by semver
|
||||||
|
for (const group of groupPages) {
|
||||||
|
if (group.pages && Array.isArray(group.pages)) {
|
||||||
|
group.pages = sortPagesBySemver(group.pages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync('docs/docs.json', JSON.stringify(docs, null, 2) + '\n');
|
||||||
|
console.log('✅ Updated docs.json');
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pulling again before committing
|
||||||
|
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||||
|
if [ "$CURRENT_BRANCH" = "HEAD" ]; then
|
||||||
|
# In detached HEAD state (common in CI), use GITHUB_REF_NAME or default to main
|
||||||
|
CURRENT_BRANCH="${GITHUB_REF_NAME:-main}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Pulling latest changes from origin/$CURRENT_BRANCH..."
|
||||||
|
if ! git pull origin "$CURRENT_BRANCH"; then
|
||||||
|
echo "❌ Error: git pull origin $CURRENT_BRANCH failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Commit and push changes
|
||||||
|
git add "docs/changelogs/$VERSION_WITH_PREFIX.mdx"
|
||||||
|
git add docs/docs.json
|
||||||
|
git add "$CLI_CHANGELOG_PATH"
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git commit -m "Adds CLI changelog for v$VERSION --skip-ci"
|
||||||
|
git push origin "$CURRENT_BRANCH"
|
||||||
|
|
||||||
|
echo "✅ Pushed CLI changelog for v$VERSION"
|
||||||
286
.github/workflows/scripts/push-mintlify-changelog.sh
vendored
Executable file
286
.github/workflows/scripts/push-mintlify-changelog.sh
vendored
Executable file
@@ -0,0 +1,286 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
VERSION=$1
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo "Usage: $0 <version>"
|
||||||
|
echo "Example: $0 1.2.0"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="v$VERSION"
|
||||||
|
|
||||||
|
# Check if this page already exists in docs/changelogs/
|
||||||
|
if [ -f "docs/changelogs/$VERSION.mdx" ]; then
|
||||||
|
echo "✅ Changelog for $VERSION already exists"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Source changelog utilities
|
||||||
|
source "$(dirname "$0")/changelog-utils.sh"
|
||||||
|
|
||||||
|
# Get current date
|
||||||
|
CURRENT_DATE=$(date +"%Y-%m-%d")
|
||||||
|
|
||||||
|
# Preparing changelog file
|
||||||
|
CHANGELOG_BODY="---
|
||||||
|
title: \"$VERSION\"
|
||||||
|
description: \"$VERSION changelog - $CURRENT_DATE\"
|
||||||
|
---
|
||||||
|
<Tabs>
|
||||||
|
<Tab title=\"NPX\">
|
||||||
|
\`\`\`bash
|
||||||
|
npx -y @maximhq/bifrost --transport-version $VERSION
|
||||||
|
\`\`\`
|
||||||
|
</Tab>
|
||||||
|
<Tab title=\"Docker\">
|
||||||
|
\`\`\`bash
|
||||||
|
docker pull maximhq/bifrost:$VERSION
|
||||||
|
docker run -p 8080:8080 maximhq/bifrost:$VERSION
|
||||||
|
\`\`\`
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
"
|
||||||
|
|
||||||
|
# Array to track cleaned changelog files
|
||||||
|
CLEANED_CHANGELOG_FILES=()
|
||||||
|
|
||||||
|
# Helper to append a section if changelog file exists and is non-empty
|
||||||
|
append_section () {
|
||||||
|
label=$1
|
||||||
|
path=$2
|
||||||
|
if [ -f "$path" ]; then
|
||||||
|
# Get changelog content
|
||||||
|
content=$(get_file_content "$path")
|
||||||
|
# If changelog is empty, skip
|
||||||
|
if [ -z "$content" ]; then
|
||||||
|
echo "❌ Changelog is empty"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
# Remove /changelog.md from the path and add /version
|
||||||
|
version_file_path="${path%/changelog.md}/version"
|
||||||
|
# Get version content
|
||||||
|
version_body=$(get_file_content "$version_file_path")
|
||||||
|
# Build the changelog section
|
||||||
|
CHANGELOG_BODY+=$'\n'"<Update label=\"$label\" description=\"$version_body\">"$'\n'"$content"$'\n\n'"</Update>"
|
||||||
|
# Clear the changelog file after processing
|
||||||
|
printf '' > "$path"
|
||||||
|
# Track this file for git commit
|
||||||
|
CLEANED_CHANGELOG_FILES+=("$path")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP changelog
|
||||||
|
append_section "Bifrost(HTTP)" transports/changelog.md
|
||||||
|
|
||||||
|
# Core changelog
|
||||||
|
append_section "Core" core/changelog.md
|
||||||
|
|
||||||
|
# Framework changelog
|
||||||
|
append_section "Framework" framework/changelog.md
|
||||||
|
|
||||||
|
# Plugins changelogs
|
||||||
|
for plugin in plugins/*; do
|
||||||
|
name=$(basename "$plugin")
|
||||||
|
append_section "$name" "$plugin/changelog.md"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Write to file
|
||||||
|
mkdir -p docs/changelogs
|
||||||
|
echo "$CHANGELOG_BODY" > docs/changelogs/$VERSION.mdx
|
||||||
|
|
||||||
|
# Update docs.json to include this new changelog route in the Changelogs tab pages array
|
||||||
|
# Uses month-based grouping: current month at top level, older months in groups
|
||||||
|
# Automatically reorganizes when month changes
|
||||||
|
route="changelogs/$VERSION"
|
||||||
|
if ! grep -q "\"$route\"" docs/docs.json; then
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const docs = JSON.parse(fs.readFileSync('docs/docs.json', 'utf8'));
|
||||||
|
|
||||||
|
// Semantic version comparison function
|
||||||
|
// Extracts version from route/filename and compares in descending order (newest first)
|
||||||
|
function compareVersionsDesc(a, b) {
|
||||||
|
// Extract route string from string or object
|
||||||
|
const routeA = typeof a === 'string' ? a : '';
|
||||||
|
const routeB = typeof b === 'string' ? b : '';
|
||||||
|
|
||||||
|
// Extract version from route (e.g., 'changelogs/v1.3.34' -> 'v1.3.34')
|
||||||
|
const versionA = routeA.split('/').pop() || '';
|
||||||
|
const versionB = routeB.split('/').pop() || '';
|
||||||
|
|
||||||
|
// Remove 'v' prefix and split into parts
|
||||||
|
const partsA = versionA.replace(/^v/, '').split(/[.-]/).map(p => {
|
||||||
|
const num = parseInt(p, 10);
|
||||||
|
return isNaN(num) ? p : num;
|
||||||
|
});
|
||||||
|
const partsB = versionB.replace(/^v/, '').split(/[.-]/).map(p => {
|
||||||
|
const num = parseInt(p, 10);
|
||||||
|
return isNaN(num) ? p : num;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compare each part (major, minor, patch, pre-release, etc.)
|
||||||
|
const maxLength = Math.max(partsA.length, partsB.length);
|
||||||
|
for (let i = 0; i < maxLength; i++) {
|
||||||
|
// Release vs prerelease: release is newer (no suffix > has suffix)
|
||||||
|
if (partsA[i] === undefined && partsB[i] !== undefined) {
|
||||||
|
return -1; // A (release) comes first in descending order
|
||||||
|
}
|
||||||
|
if (partsB[i] === undefined && partsA[i] !== undefined) {
|
||||||
|
return 1; // B (release) comes first in descending order
|
||||||
|
}
|
||||||
|
|
||||||
|
const partA = partsA[i];
|
||||||
|
const partB = partsB[i];
|
||||||
|
|
||||||
|
// If both are numbers, compare numerically
|
||||||
|
if (typeof partA === 'number' && typeof partB === 'number') {
|
||||||
|
if (partA !== partB) {
|
||||||
|
return partB - partA; // Descending order
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle prerelease strings with numeric suffixes (e.g., 'prerelease10')
|
||||||
|
const strA = String(partA);
|
||||||
|
const strB = String(partB);
|
||||||
|
const matchA = strA.match(/^([a-zA-Z]+)(\\d+)$/);
|
||||||
|
const matchB = strB.match(/^([a-zA-Z]+)(\\d+)$/);
|
||||||
|
|
||||||
|
if (matchA && matchB && matchA[1] === matchB[1]) {
|
||||||
|
// Same prefix, compare numbers numerically
|
||||||
|
const numA = parseInt(matchA[2], 10);
|
||||||
|
const numB = parseInt(matchB[2], 10);
|
||||||
|
if (numA !== numB) {
|
||||||
|
return numB - numA; // Descending order
|
||||||
|
}
|
||||||
|
} else if (strA !== strB) {
|
||||||
|
return strB.localeCompare(strA); // Descending order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0; // Equal
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort a pages array by semver (descending)
|
||||||
|
function sortPagesBySemver(pages) {
|
||||||
|
return pages.slice().sort(compareVersionsDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current month/year
|
||||||
|
const releaseDate = new Date('$CURRENT_DATE');
|
||||||
|
const currentDate = new Date();
|
||||||
|
const releaseMonthYear = releaseDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
|
||||||
|
const currentMonthYear = currentDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
|
||||||
|
|
||||||
|
// Find the Changelogs tab
|
||||||
|
const changelogsTab = docs.navigation.tabs.find(tab => tab.tab === 'Changelogs');
|
||||||
|
if (!changelogsTab) {
|
||||||
|
console.error('Changelogs tab not found');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the Open Source menu item
|
||||||
|
const openSourceItem = changelogsTab.menu?.find(item => item.item === 'Open Source');
|
||||||
|
if (!openSourceItem) {
|
||||||
|
console.error('Open Source menu item not found in Changelogs tab');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all top-level entries and existing groups
|
||||||
|
const topLevelEntries = openSourceItem.pages.filter(p => typeof p === 'string');
|
||||||
|
const existingGroups = openSourceItem.pages.filter(p => typeof p === 'object');
|
||||||
|
|
||||||
|
// Check if we need to group existing top-level entries
|
||||||
|
if (topLevelEntries.length > 0) {
|
||||||
|
// Get the month of the first top-level entry (they should all be from same month)
|
||||||
|
const firstEntryPath = topLevelEntries[0].replace('changelogs/', '') + '.mdx';
|
||||||
|
const firstEntryFile = 'docs/changelogs/' + firstEntryPath;
|
||||||
|
|
||||||
|
let topLevelMonth = null;
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(firstEntryFile, 'utf8');
|
||||||
|
const descMatch = content.match(/description:\\s*\"[^\"]*?(\\d{4}-\\d{2}-\\d{2})[^\"]*\"/);
|
||||||
|
if (descMatch) {
|
||||||
|
const entryDate = new Date(descMatch[1]);
|
||||||
|
topLevelMonth = entryDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(\`Warning: Could not read entry file \${firstEntryFile}: \${e.message}\`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only group if the month has changed
|
||||||
|
if (topLevelMonth && topLevelMonth !== releaseMonthYear) {
|
||||||
|
console.log(\`📦 Month changed from \${topLevelMonth} to \${releaseMonthYear}\`);
|
||||||
|
console.log(\`📦 Grouping \${topLevelEntries.length} top-level entries into \${topLevelMonth} group...\`);
|
||||||
|
|
||||||
|
// Create a group for all existing top-level entries
|
||||||
|
const previousMonthGroup = {
|
||||||
|
group: topLevelMonth,
|
||||||
|
pages: sortPagesBySemver(topLevelEntries)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add this group at the top of existing groups
|
||||||
|
existingGroups.unshift(previousMonthGroup);
|
||||||
|
console.log(\`✅ Created \${topLevelMonth} group with \${topLevelEntries.length} entries (sorted)\`);
|
||||||
|
|
||||||
|
// Clear top-level entries (they're now in the group)
|
||||||
|
openSourceItem.pages = existingGroups;
|
||||||
|
} else {
|
||||||
|
console.log(\`📋 Same month (\${releaseMonthYear}), keeping existing top-level entries\`);
|
||||||
|
// Keep existing structure (top-level entries + groups)
|
||||||
|
openSourceItem.pages = [...topLevelEntries, ...existingGroups];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRoute = '$route';
|
||||||
|
|
||||||
|
// Add the new changelog at the top level
|
||||||
|
openSourceItem.pages.unshift(newRoute);
|
||||||
|
console.log(\`✅ Added \${newRoute} to top level\`);
|
||||||
|
|
||||||
|
// Sort the top-level pages array by semver
|
||||||
|
const topLevelPages = openSourceItem.pages.filter(p => typeof p === 'string');
|
||||||
|
const groupPages = openSourceItem.pages.filter(p => typeof p === 'object');
|
||||||
|
|
||||||
|
if (topLevelPages.length > 0) {
|
||||||
|
const sortedTopLevel = sortPagesBySemver(topLevelPages);
|
||||||
|
openSourceItem.pages = [...sortedTopLevel, ...groupPages];
|
||||||
|
console.log(\`✅ Sorted \${topLevelPages.length} top-level pages by semver\`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort each group's pages by semver
|
||||||
|
for (const group of groupPages) {
|
||||||
|
if (group.pages && Array.isArray(group.pages)) {
|
||||||
|
group.pages = sortPagesBySemver(group.pages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync('docs/docs.json', JSON.stringify(docs, null, 2) + '\n');
|
||||||
|
console.log('✅ Updated docs.json');
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pulling again before committing
|
||||||
|
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||||
|
if [ "$CURRENT_BRANCH" = "HEAD" ]; then
|
||||||
|
# In detached HEAD state (common in CI), use GITHUB_REF_NAME or default to main
|
||||||
|
CURRENT_BRANCH="${GITHUB_REF_NAME:-main}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Pulling latest changes from origin/$CURRENT_BRANCH..."
|
||||||
|
if ! git pull origin "$CURRENT_BRANCH"; then
|
||||||
|
echo "❌ Error: git pull origin $CURRENT_BRANCH failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Commit and push changes
|
||||||
|
git add docs/changelogs/$VERSION.mdx
|
||||||
|
git add docs/docs.json
|
||||||
|
# Add all cleaned changelog files
|
||||||
|
for file in "${CLEANED_CHANGELOG_FILES[@]}"; do
|
||||||
|
git add "$file"
|
||||||
|
done
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git commit -m "Adds changelog for $VERSION --skip-ci"
|
||||||
|
git push origin "$CURRENT_BRANCH"
|
||||||
110
.github/workflows/scripts/release-all-plugins.sh
vendored
Executable file
110
.github/workflows/scripts/release-all-plugins.sh
vendored
Executable file
@@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Release all changed plugins sequentially
|
||||||
|
# Usage: ./release-all-plugins.sh '["plugin1", "plugin2"]'
|
||||||
|
|
||||||
|
# Validate that an argument was provided
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
echo "❌ Error: Missing required argument"
|
||||||
|
echo "Usage: $0 '<JSON_ARRAY_OF_PLUGINS>'"
|
||||||
|
echo "Example: $0 '[\"plugin1\", \"plugin2\"]'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CHANGED_PLUGINS_JSON="$1"
|
||||||
|
|
||||||
|
# Verify jq is available
|
||||||
|
if ! command -v jq >/dev/null 2>&1; then
|
||||||
|
echo "❌ Error: jq is required but not installed"
|
||||||
|
echo "Please install jq to parse JSON input"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate that the input is valid JSON
|
||||||
|
if ! echo "$CHANGED_PLUGINS_JSON" | jq empty >/dev/null 2>&1; then
|
||||||
|
echo "❌ Error: Invalid JSON provided"
|
||||||
|
echo "Input: $CHANGED_PLUGINS_JSON"
|
||||||
|
echo "Please provide a valid JSON array of plugin names"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
echo "🔌 Processing plugin releases..."
|
||||||
|
echo "📋 Changed plugins JSON: $CHANGED_PLUGINS_JSON"
|
||||||
|
|
||||||
|
# No work early‐exit if array is empty
|
||||||
|
if jq -e 'length==0' <<<"$CHANGED_PLUGINS_JSON" >/dev/null 2>&1; then
|
||||||
|
echo "⏭️ No plugins to release"
|
||||||
|
echo "success=true" >> "${GITHUB_OUTPUT:-/dev/null}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Convert JSON array to bash array using readarray to avoid word-splitting
|
||||||
|
if ! readarray -t PLUGINS < <(echo "$CHANGED_PLUGINS_JSON" | jq -r '.[]' 2>/dev/null); then
|
||||||
|
echo "❌ Error: Failed to parse plugin names from JSON"
|
||||||
|
echo "Input: $CHANGED_PLUGINS_JSON"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify release-single-plugin.sh exists and is executable
|
||||||
|
RELEASE_SCRIPT="./.github/workflows/scripts/release-single-plugin.sh"
|
||||||
|
if [ ! -f "$RELEASE_SCRIPT" ]; then
|
||||||
|
echo "❌ Error: Release script not found: $RELEASE_SCRIPT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$RELEASE_SCRIPT" ]; then
|
||||||
|
echo "❌ Error: Release script is not executable: $RELEASE_SCRIPT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#PLUGINS[@]} -eq 0 ]; then
|
||||||
|
echo "⏭️ No plugins to release"
|
||||||
|
echo "success=true" >> "${GITHUB_OUTPUT:-/dev/null}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔄 Releasing ${#PLUGINS[@]} plugins:"
|
||||||
|
for p in "${PLUGINS[@]}"; do
|
||||||
|
echo " • $p"
|
||||||
|
done
|
||||||
|
|
||||||
|
FAILED_PLUGINS=()
|
||||||
|
SUCCESS_COUNT=0
|
||||||
|
OVERALL_EXIT_CODE=0
|
||||||
|
|
||||||
|
# Release each plugin
|
||||||
|
for plugin in "${PLUGINS[@]}"; do
|
||||||
|
echo ""
|
||||||
|
echo "🔌 Releasing plugin: $plugin"
|
||||||
|
|
||||||
|
# Capture the exit code of the plugin release
|
||||||
|
if "$RELEASE_SCRIPT" "$plugin"; then
|
||||||
|
PLUGIN_EXIT_CODE=$?
|
||||||
|
echo "✅ Successfully released: $plugin"
|
||||||
|
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||||
|
else
|
||||||
|
PLUGIN_EXIT_CODE=$?
|
||||||
|
echo "❌ Failed to release plugin '$plugin' (exit code: $PLUGIN_EXIT_CODE)"
|
||||||
|
FAILED_PLUGINS+=("$plugin")
|
||||||
|
OVERALL_EXIT_CODE=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo ""
|
||||||
|
echo "📋 Plugin Release Summary:"
|
||||||
|
echo " ✅ Successful: $SUCCESS_COUNT/${#PLUGINS[@]}"
|
||||||
|
echo " ❌ Failed: ${#FAILED_PLUGINS[@]}"
|
||||||
|
|
||||||
|
if [ ${#FAILED_PLUGINS[@]} -gt 0 ]; then
|
||||||
|
echo " Failed plugins: ${FAILED_PLUGINS[*]}"
|
||||||
|
echo "success=false" >> "${GITHUB_OUTPUT:-/dev/null}"
|
||||||
|
echo "❌ Plugin release process completed with failures"
|
||||||
|
exit $OVERALL_EXIT_CODE
|
||||||
|
else
|
||||||
|
echo " 🎉 All plugins released successfully!"
|
||||||
|
echo "success=true" >> "${GITHUB_OUTPUT:-/dev/null}"
|
||||||
|
echo "✅ All plugin releases completed successfully"
|
||||||
|
fi
|
||||||
199
.github/workflows/scripts/release-bifrost-http-finalize.sh
vendored
Executable file
199
.github/workflows/scripts/release-bifrost-http-finalize.sh
vendored
Executable file
@@ -0,0 +1,199 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Finalize bifrost-http release: changelog, tagging, GitHub release, R2 latest copy
|
||||||
|
# Usage: ./release-bifrost-http-finalize.sh <version>
|
||||||
|
|
||||||
|
# Validate input argument
|
||||||
|
if [ "${1:-}" = "" ]; then
|
||||||
|
echo "Usage: $0 <version>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="$1"
|
||||||
|
TAG_NAME="transports/v${VERSION}"
|
||||||
|
|
||||||
|
echo "🏷️ Finalizing bifrost-http v$VERSION release..."
|
||||||
|
|
||||||
|
# Get core and framework versions from version files
|
||||||
|
CORE_VERSION="v$(tr -d '\n\r' < core/version)"
|
||||||
|
FRAMEWORK_VERSION="v$(tr -d '\n\r' < framework/version)"
|
||||||
|
|
||||||
|
# Re-compute plugin versions from version files and transports/go.mod
|
||||||
|
declare -A PLUGIN_VERSIONS
|
||||||
|
PLUGINS_USED=()
|
||||||
|
|
||||||
|
for plugin_dir in plugins/*/; do
|
||||||
|
if [ -d "$plugin_dir" ]; then
|
||||||
|
plugin_name=$(basename "$plugin_dir")
|
||||||
|
PLUGIN_VERSION="v$(tr -d '\n\r' < "${plugin_dir}version")"
|
||||||
|
PLUGIN_VERSIONS["$plugin_name"]="$PLUGIN_VERSION"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check which plugins are actually used by the transport
|
||||||
|
while IFS= read -r plugin_line; do
|
||||||
|
plugin_name=$(echo "$plugin_line" | awk -F'/' '{print $NF}' | awk '{print $1}')
|
||||||
|
plugin_version=$(echo "$plugin_line" | awk '{print $NF}')
|
||||||
|
|
||||||
|
# Use version file version if available, otherwise use go.mod version
|
||||||
|
if [[ -n "${PLUGIN_VERSIONS[$plugin_name]:-}" ]]; then
|
||||||
|
PLUGINS_USED+=("$plugin_name:${PLUGIN_VERSIONS[$plugin_name]}")
|
||||||
|
else
|
||||||
|
PLUGIN_VERSIONS["$plugin_name"]="$plugin_version"
|
||||||
|
PLUGINS_USED+=("$plugin_name:$plugin_version")
|
||||||
|
fi
|
||||||
|
done < <(grep "github.com/maximhq/bifrost/plugins/" transports/go.mod)
|
||||||
|
|
||||||
|
echo "🔧 Versions:"
|
||||||
|
echo " Core: $CORE_VERSION"
|
||||||
|
echo " Framework: $FRAMEWORK_VERSION"
|
||||||
|
echo " Plugins:"
|
||||||
|
for plugin_name in "${!PLUGIN_VERSIONS[@]}"; do
|
||||||
|
echo " - $plugin_name: ${PLUGIN_VERSIONS[$plugin_name]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Capturing changelog
|
||||||
|
CHANGELOG_BODY=$(cat transports/changelog.md)
|
||||||
|
# Skip comments from changelog
|
||||||
|
CHANGELOG_BODY=$(echo "$CHANGELOG_BODY" | grep -v '^<!--' | grep -v '^-->')
|
||||||
|
# If changelog is empty, return error
|
||||||
|
if [ -z "$CHANGELOG_BODY" ]; then
|
||||||
|
echo "❌ Changelog is empty"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "📝 New changelog: $CHANGELOG_BODY"
|
||||||
|
|
||||||
|
# Finding previous tag
|
||||||
|
echo "🔍 Finding previous tag..."
|
||||||
|
PREV_TAG=$(git tag -l "transports/v*" | sort -V | tail -1)
|
||||||
|
if [[ "$PREV_TAG" == "$TAG_NAME" ]]; then
|
||||||
|
PREV_TAG=$(git tag -l "transports/v*" | sort -V | tail -2 | head -1)
|
||||||
|
fi
|
||||||
|
echo "🔍 Previous tag: $PREV_TAG"
|
||||||
|
|
||||||
|
# Get message of the tag
|
||||||
|
echo "🔍 Getting previous tag message..."
|
||||||
|
PREV_CHANGELOG=$(git tag -l --format='%(contents)' "$PREV_TAG")
|
||||||
|
echo "📝 Previous changelog body: $PREV_CHANGELOG"
|
||||||
|
|
||||||
|
# Checking if tag message is the same as the changelog
|
||||||
|
if [[ "$PREV_CHANGELOG" == "$CHANGELOG_BODY" ]]; then
|
||||||
|
echo "❌ Changelog is the same as the previous changelog"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create and push tag
|
||||||
|
echo "🏷️ Creating tag: $TAG_NAME"
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git tag "$TAG_NAME" -m "Release transports v$VERSION" -m "$CHANGELOG_BODY"
|
||||||
|
git push origin "$TAG_NAME"
|
||||||
|
|
||||||
|
# Create GitHub release
|
||||||
|
TITLE="Bifrost HTTP v$VERSION"
|
||||||
|
|
||||||
|
# Mark prereleases when version contains a hyphen
|
||||||
|
PRERELEASE_FLAG=""
|
||||||
|
if [[ "$VERSION" == *-* ]]; then
|
||||||
|
PRERELEASE_FLAG="--prerelease"
|
||||||
|
fi
|
||||||
|
|
||||||
|
LATEST_FLAG=""
|
||||||
|
if [[ "$VERSION" != *-* ]]; then
|
||||||
|
LATEST_FLAG="--latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate plugin version summary
|
||||||
|
PLUGIN_UPDATES=""
|
||||||
|
if [ ${#PLUGINS_USED[@]} -gt 0 ]; then
|
||||||
|
PLUGIN_UPDATES="
|
||||||
|
|
||||||
|
### 🔌 Plugin Versions
|
||||||
|
This release includes the following plugin versions:
|
||||||
|
"
|
||||||
|
for plugin_info in "${PLUGINS_USED[@]}"; do
|
||||||
|
plugin_name="${plugin_info%%:*}"
|
||||||
|
plugin_version="${plugin_info##*:}"
|
||||||
|
PLUGIN_UPDATES="$PLUGIN_UPDATES- **$plugin_name**: \`$plugin_version\`
|
||||||
|
"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
# Show all available plugin versions even if not directly used
|
||||||
|
PLUGIN_UPDATES="
|
||||||
|
|
||||||
|
### 🔌 Available Plugin Versions
|
||||||
|
The following plugin versions are compatible with this release:
|
||||||
|
"
|
||||||
|
for plugin_name in "${!PLUGIN_VERSIONS[@]}"; do
|
||||||
|
plugin_version="${PLUGIN_VERSIONS[$plugin_name]}"
|
||||||
|
PLUGIN_UPDATES="$PLUGIN_UPDATES- **$plugin_name**: \`$plugin_version\`
|
||||||
|
"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
BODY="## Bifrost HTTP Transport Release v$VERSION
|
||||||
|
|
||||||
|
$CHANGELOG_BODY
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
#### Docker
|
||||||
|
\`\`\`bash
|
||||||
|
docker run -p 8080:8080 maximhq/bifrost:v$VERSION
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
#### Binary Download
|
||||||
|
\`\`\`bash
|
||||||
|
npx @maximhq/bifrost --transport-version v$VERSION
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Docker Images
|
||||||
|
- **\`maximhq/bifrost:v$VERSION\`** - This specific version
|
||||||
|
- **\`maximhq/bifrost:latest\`** - Latest version (updated with this release)
|
||||||
|
|
||||||
|
---
|
||||||
|
_This release was automatically created with dependencies: core \`$CORE_VERSION\`, framework \`$FRAMEWORK_VERSION\`. All plugins have been validated and updated._"
|
||||||
|
|
||||||
|
if [ -z "${GH_TOKEN:-}" ] && [ -z "${GITHUB_TOKEN:-}" ]; then
|
||||||
|
echo "Error: GH_TOKEN or GITHUB_TOKEN is not set. Please export one to authenticate the GitHub CLI."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🎉 Creating GitHub release for $TITLE..."
|
||||||
|
gh release create "$TAG_NAME" \
|
||||||
|
--title "$TITLE" \
|
||||||
|
--notes "$BODY" \
|
||||||
|
${PRERELEASE_FLAG} ${LATEST_FLAG}
|
||||||
|
|
||||||
|
echo "✅ Bifrost HTTP released successfully"
|
||||||
|
|
||||||
|
# Copy versioned R2 path to latest/ for stable releases
|
||||||
|
if [[ "$VERSION" != *-* ]]; then
|
||||||
|
if [ -n "${R2_ENDPOINT:-}" ] && [ -n "${R2_BUCKET:-}" ]; then
|
||||||
|
echo "📤 Copying versioned binaries to latest/ on R2..."
|
||||||
|
R2_ENDPOINT="$(echo "$R2_ENDPOINT" | tr -d '[:space:]')"
|
||||||
|
aws s3 sync "s3://$R2_BUCKET/bifrost/v$VERSION/" "s3://$R2_BUCKET/bifrost/latest/" \
|
||||||
|
--endpoint-url "$R2_ENDPOINT" \
|
||||||
|
--profile "${R2_AWS_PROFILE:-R2}" \
|
||||||
|
--no-progress \
|
||||||
|
--delete
|
||||||
|
echo "✅ Latest binaries updated on R2"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
echo ""
|
||||||
|
echo "📋 Release Summary:"
|
||||||
|
echo " 🏷️ Tag: $TAG_NAME"
|
||||||
|
echo " 🔧 Core version: $CORE_VERSION"
|
||||||
|
echo " 🔧 Framework version: $FRAMEWORK_VERSION"
|
||||||
|
echo " 📦 Transport: Updated"
|
||||||
|
if [ ${#PLUGINS_USED[@]} -gt 0 ]; then
|
||||||
|
echo " 🔌 Plugins used: ${PLUGINS_USED[*]}"
|
||||||
|
else
|
||||||
|
echo " 🔌 Available plugins: $(printf "%s " "${!PLUGIN_VERSIONS[@]}")"
|
||||||
|
fi
|
||||||
|
echo " 🎉 GitHub release: Created"
|
||||||
|
|
||||||
|
echo "success=true" >> "$GITHUB_OUTPUT"
|
||||||
157
.github/workflows/scripts/release-bifrost-http-prep.sh
vendored
Executable file
157
.github/workflows/scripts/release-bifrost-http-prep.sh
vendored
Executable file
@@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Prepare bifrost-http release: update dependencies, build UI, validate, commit/push
|
||||||
|
# Usage: ./release-bifrost-http-prep.sh <version>
|
||||||
|
|
||||||
|
# Get the absolute path of the script directory
|
||||||
|
# Use readlink if available (Linux), otherwise use cd/pwd (macOS compatible)
|
||||||
|
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||||
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
|
else
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Source Go utilities for exponential backoff
|
||||||
|
source "$SCRIPT_DIR/go-utils.sh"
|
||||||
|
|
||||||
|
# Validate input argument
|
||||||
|
if [ "${1:-}" = "" ]; then
|
||||||
|
echo "Usage: $0 <version>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="$1"
|
||||||
|
|
||||||
|
echo "🚀 Preparing bifrost-http v$VERSION release..."
|
||||||
|
|
||||||
|
# Get core and framework versions from version files
|
||||||
|
CORE_VERSION="v$(tr -d '\n\r' < core/version)"
|
||||||
|
FRAMEWORK_VERSION="v$(tr -d '\n\r' < framework/version)"
|
||||||
|
|
||||||
|
echo "🔍 DEBUG: CORE_VERSION: $CORE_VERSION"
|
||||||
|
echo "🔍 DEBUG: FRAMEWORK_VERSION: $FRAMEWORK_VERSION"
|
||||||
|
|
||||||
|
|
||||||
|
# Get plugin versions from version files
|
||||||
|
echo "🔌 Getting plugin versions from version files..."
|
||||||
|
declare -A PLUGIN_VERSIONS
|
||||||
|
|
||||||
|
# Get versions for plugins that exist in the plugins/ directory
|
||||||
|
for plugin_dir in plugins/*/; do
|
||||||
|
if [ -d "$plugin_dir" ]; then
|
||||||
|
plugin_name=$(basename "$plugin_dir")
|
||||||
|
PLUGIN_VERSION="v$(tr -d '\n\r' < "${plugin_dir}version")"
|
||||||
|
PLUGIN_VERSIONS["$plugin_name"]="$PLUGIN_VERSION"
|
||||||
|
echo " 📦 $plugin_name: $PLUGIN_VERSION (from version file)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Also check for any plugins already in transport go.mod that might not be in plugins/ directory
|
||||||
|
cd transports
|
||||||
|
echo "🔍 Checking for additional plugins in transport go.mod..."
|
||||||
|
# Parse go.mod plugin lines and add missing ones
|
||||||
|
while IFS= read -r plugin_line; do
|
||||||
|
plugin_name=$(echo "$plugin_line" | awk -F'/' '{print $NF}' | awk '{print $1}')
|
||||||
|
current_version=$(echo "$plugin_line" | awk '{print $NF}')
|
||||||
|
|
||||||
|
# Only add if we don't already have this plugin
|
||||||
|
if [[ -z "${PLUGIN_VERSIONS[$plugin_name]:-}" ]]; then
|
||||||
|
echo " 📦 $plugin_name: $current_version (from transport go.mod)"
|
||||||
|
PLUGIN_VERSIONS["$plugin_name"]="$current_version"
|
||||||
|
fi
|
||||||
|
done < <(grep "github.com/maximhq/bifrost/plugins/" go.mod)
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
echo "🔧 Using versions:"
|
||||||
|
echo " Core: $CORE_VERSION"
|
||||||
|
echo " Framework: $FRAMEWORK_VERSION"
|
||||||
|
echo " Plugins:"
|
||||||
|
for plugin_name in "${!PLUGIN_VERSIONS[@]}"; do
|
||||||
|
echo " - $plugin_name: ${PLUGIN_VERSIONS[$plugin_name]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Update transport dependencies to use plugin versions from version files
|
||||||
|
echo "🔧 Using plugin versions from version files for transport..."
|
||||||
|
|
||||||
|
# Track which plugins are actually used by the transport
|
||||||
|
cd transports
|
||||||
|
|
||||||
|
# Normalize the local go.mod directive up front so prior-release artifacts
|
||||||
|
# (e.g. `go 1.26.2` written by earlier `go get` runs) don't trip GOTOOLCHAIN=local.
|
||||||
|
go mod edit -go=1.26.1 -toolchain=none
|
||||||
|
|
||||||
|
for plugin_name in "${!PLUGIN_VERSIONS[@]}"; do
|
||||||
|
plugin_version="${PLUGIN_VERSIONS[$plugin_name]}"
|
||||||
|
|
||||||
|
# Check if transport depends on this plugin
|
||||||
|
if grep -q "github.com/maximhq/bifrost/plugins/$plugin_name" go.mod; then
|
||||||
|
echo " 📦 Using $plugin_name plugin $plugin_version"
|
||||||
|
# Textual require bump — skips loading the currently-declared version's go.mod
|
||||||
|
go mod edit -require="github.com/maximhq/bifrost/plugins/$plugin_name@$plugin_version"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Also ensure core and framework are up to date
|
||||||
|
|
||||||
|
echo " 🔧 Updating core to $CORE_VERSION"
|
||||||
|
go mod edit -require="github.com/maximhq/bifrost/core@$CORE_VERSION"
|
||||||
|
|
||||||
|
echo " 📦 Updating framework to $FRAMEWORK_VERSION"
|
||||||
|
go mod edit -require="github.com/maximhq/bifrost/framework@$FRAMEWORK_VERSION"
|
||||||
|
|
||||||
|
# Re-normalize before tidy in case any edit reintroduced a toolchain line
|
||||||
|
go mod edit -go=1.26.1 -toolchain=none
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# We need to build UI first before we can validate the transport build
|
||||||
|
echo "🎨 Building UI..."
|
||||||
|
make build-ui
|
||||||
|
|
||||||
|
# Building hello-world plugin
|
||||||
|
echo "🔨 Building hello-world plugin..."
|
||||||
|
cd examples/plugins/hello-world
|
||||||
|
make build
|
||||||
|
cd ../../..
|
||||||
|
|
||||||
|
# Validate transport build
|
||||||
|
echo "🔨 Validating transport build..."
|
||||||
|
cd transports
|
||||||
|
go build ./...
|
||||||
|
cd ..
|
||||||
|
echo "✅ Transport build validation successful"
|
||||||
|
|
||||||
|
# Note: Migration tests run as a separate CI job (test-migrations) before this release job
|
||||||
|
|
||||||
|
# Commit and push changes if any
|
||||||
|
# First, pull latest changes to avoid conflicts
|
||||||
|
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||||
|
if [ "$CURRENT_BRANCH" = "HEAD" ]; then
|
||||||
|
# In detached HEAD state (common in CI), use GITHUB_REF_NAME or default to main
|
||||||
|
CURRENT_BRANCH="${GITHUB_REF_NAME:-main}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Pulling latest changes from origin/$CURRENT_BRANCH..."
|
||||||
|
if ! git pull origin "$CURRENT_BRANCH"; then
|
||||||
|
echo "❌ Error: git pull origin $CURRENT_BRANCH failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stage any changes made to transports/
|
||||||
|
git add transports/
|
||||||
|
|
||||||
|
# Check if there are staged changes after pulling
|
||||||
|
if ! git diff --cached --quiet; then
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
echo "🔧 Committing and pushing changes..."
|
||||||
|
git commit -m "transports: update dependencies --skip-ci"
|
||||||
|
git push -u origin HEAD
|
||||||
|
else
|
||||||
|
echo "ℹ️ No staged changes to commit"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Prep complete for bifrost-http v$VERSION"
|
||||||
|
echo "success=true" >> "$GITHUB_OUTPUT"
|
||||||
143
.github/workflows/scripts/release-cli.sh
vendored
Executable file
143
.github/workflows/scripts/release-cli.sh
vendored
Executable file
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Release bifrost CLI component
|
||||||
|
# Usage: ./release-cli.sh <version>
|
||||||
|
|
||||||
|
# Get the absolute path of the script directory
|
||||||
|
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||||
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
|
else
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
fi
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd -P)"
|
||||||
|
|
||||||
|
# Validate input argument
|
||||||
|
if [ "${1:-}" = "" ]; then
|
||||||
|
echo "Usage: $0 <version>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="$1"
|
||||||
|
TAG_NAME="cli/v${VERSION}"
|
||||||
|
|
||||||
|
echo "🚀 Releasing bifrost CLI v$VERSION..."
|
||||||
|
|
||||||
|
# Validate CLI build
|
||||||
|
echo "🔨 Validating CLI build..."
|
||||||
|
COMMIT="${GITHUB_SHA:-$(git rev-parse HEAD 2>/dev/null || echo 'unknown')}"
|
||||||
|
(cd "$REPO_ROOT/cli" && go build -ldflags "-X main.version=v${VERSION} -X main.commit=${COMMIT}" ./...)
|
||||||
|
echo "✅ CLI build validation successful"
|
||||||
|
|
||||||
|
# Build CLI executables
|
||||||
|
echo "🔨 Building executables..."
|
||||||
|
bash "$SCRIPT_DIR/build-cli-executables.sh" "$VERSION"
|
||||||
|
|
||||||
|
# --- Preflight checks (no side effects) ---
|
||||||
|
|
||||||
|
# Capturing changelog
|
||||||
|
CHANGELOG_BODY=$(cat "$REPO_ROOT/cli/changelog.md")
|
||||||
|
# Skip comments from changelog
|
||||||
|
CHANGELOG_BODY=$(printf '%s\n' "$CHANGELOG_BODY" | sed '/<!--/,/-->/d')
|
||||||
|
# If changelog is empty, return error
|
||||||
|
if [ -z "$CHANGELOG_BODY" ]; then
|
||||||
|
echo "❌ Changelog is empty"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "📝 New changelog: $CHANGELOG_BODY"
|
||||||
|
|
||||||
|
# Finding previous tag
|
||||||
|
echo "🔍 Finding previous tag..."
|
||||||
|
TAG_COUNT=$(git tag -l "cli/v*" | wc -l | tr -d ' ')
|
||||||
|
if [[ "$TAG_COUNT" -eq 0 ]]; then
|
||||||
|
PREV_TAG=""
|
||||||
|
else
|
||||||
|
PREV_TAG=$(git tag -l "cli/v*" | sort -V | tail -1)
|
||||||
|
if [[ "$PREV_TAG" == "$TAG_NAME" ]]; then
|
||||||
|
PREV_TAG=$(git tag -l "cli/v*" | sort -V | tail -2 | head -1)
|
||||||
|
[[ "$PREV_TAG" == "$TAG_NAME" ]] && PREV_TAG=""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "🔍 Previous tag: $PREV_TAG"
|
||||||
|
|
||||||
|
# Get message of the tag and compare changelogs
|
||||||
|
if [[ -n "$PREV_TAG" ]]; then
|
||||||
|
echo "🔍 Getting previous tag message..."
|
||||||
|
PREV_CHANGELOG=$(git tag -l --format='%(contents:body)' "$PREV_TAG")
|
||||||
|
echo "📝 Previous changelog body: $PREV_CHANGELOG"
|
||||||
|
|
||||||
|
# Checking if tag message is the same as the changelog
|
||||||
|
if [[ "$PREV_CHANGELOG" == "$CHANGELOG_BODY" ]]; then
|
||||||
|
echo "❌ Changelog is the same as the previous changelog"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "ℹ️ No previous CLI tag found. Skipping changelog comparison."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify GitHub token before any publish steps
|
||||||
|
if [ -z "${GH_TOKEN:-}" ] && [ -z "${GITHUB_TOKEN:-}" ]; then
|
||||||
|
echo "Error: GH_TOKEN or GITHUB_TOKEN is not set. Please export one to authenticate the GitHub CLI."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Publish steps (all checks passed) ---
|
||||||
|
|
||||||
|
# Configure and upload to R2
|
||||||
|
echo "📤 Uploading binaries..."
|
||||||
|
bash "$SCRIPT_DIR/configure-r2.sh"
|
||||||
|
bash "$SCRIPT_DIR/upload-cli-to-r2.sh" "$TAG_NAME"
|
||||||
|
|
||||||
|
# Create and push tag
|
||||||
|
if git rev-parse -q --verify "refs/tags/$TAG_NAME" >/dev/null; then
|
||||||
|
echo "ℹ️ Tag $TAG_NAME already exists. Reusing it."
|
||||||
|
else
|
||||||
|
echo "🏷️ Creating tag: $TAG_NAME"
|
||||||
|
git tag "$TAG_NAME" -m "Release CLI v$VERSION" -m "$CHANGELOG_BODY"
|
||||||
|
git push origin "$TAG_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create GitHub release
|
||||||
|
TITLE="Bifrost CLI v$VERSION"
|
||||||
|
|
||||||
|
# Mark prereleases when version contains a hyphen
|
||||||
|
PRERELEASE_FLAG=""
|
||||||
|
if [[ "$VERSION" == *-* ]]; then
|
||||||
|
PRERELEASE_FLAG="--prerelease"
|
||||||
|
fi
|
||||||
|
|
||||||
|
BODY="## Bifrost CLI Release v$VERSION
|
||||||
|
|
||||||
|
$CHANGELOG_BODY
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
#### Binary Download
|
||||||
|
\`\`\`bash
|
||||||
|
npx @maximhq/bifrost --cli-version v$VERSION
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
_This release was automatically created._"
|
||||||
|
|
||||||
|
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
|
||||||
|
echo "ℹ️ GitHub release $TAG_NAME already exists. Skipping."
|
||||||
|
else
|
||||||
|
echo "🎉 Creating GitHub release for $TITLE..."
|
||||||
|
gh release create "$TAG_NAME" \
|
||||||
|
--title "$TITLE" \
|
||||||
|
--notes "$BODY" \
|
||||||
|
${PRERELEASE_FLAG}
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Bifrost CLI released successfully"
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
echo ""
|
||||||
|
echo "📋 Release Summary:"
|
||||||
|
echo " 🏷️ Tag: $TAG_NAME"
|
||||||
|
echo " 🎉 GitHub release: Created"
|
||||||
|
|
||||||
|
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
|
||||||
|
echo "success=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
103
.github/workflows/scripts/release-core.sh
vendored
Executable file
103
.github/workflows/scripts/release-core.sh
vendored
Executable file
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Release core component
|
||||||
|
# Usage: ./release-core.sh <version>
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "" ]]; then
|
||||||
|
echo "Usage: $0 <version>"
|
||||||
|
echo "Example: $0 1.2.0"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
VERSION="$1"
|
||||||
|
|
||||||
|
TAG_NAME="core/v${VERSION}"
|
||||||
|
|
||||||
|
echo "🔧 Releasing core v$VERSION..."
|
||||||
|
|
||||||
|
# Validate core build
|
||||||
|
echo "🔨 Validating core build..."
|
||||||
|
cd core
|
||||||
|
|
||||||
|
if [[ ! -f version ]]; then
|
||||||
|
echo "❌ Missing core/version file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
FILE_VERSION="$(cat version | tr -d '[:space:]')"
|
||||||
|
if [[ "$FILE_VERSION" != "$VERSION" ]]; then
|
||||||
|
echo "❌ Version mismatch: arg=$VERSION, core/version=$FILE_VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Capturing changelog
|
||||||
|
CHANGELOG_BODY=$(cat core/changelog.md)
|
||||||
|
# Skip comments from changelog
|
||||||
|
CHANGELOG_BODY=$(echo "$CHANGELOG_BODY" | grep -v '^<!--' | grep -v '^-->')
|
||||||
|
# If changelog is empty, return error
|
||||||
|
if [ -z "$CHANGELOG_BODY" ]; then
|
||||||
|
echo "❌ Changelog is empty"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "📝 New changelog: $CHANGELOG_BODY"
|
||||||
|
|
||||||
|
# Finding previous tag
|
||||||
|
echo "🔍 Finding previous tag..."
|
||||||
|
PREV_TAG=$(git tag -l "core/v*" | sort -V | tail -1)
|
||||||
|
if [[ "$PREV_TAG" == "$TAG_NAME" ]]; then
|
||||||
|
PREV_TAG=$(git tag -l "core/v*" | sort -V | tail -2 | head -1)
|
||||||
|
fi
|
||||||
|
echo "🔍 Previous tag: $PREV_TAG"
|
||||||
|
|
||||||
|
# Get message of the tag
|
||||||
|
echo "🔍 Getting previous tag message..."
|
||||||
|
PREV_CHANGELOG=$(git tag -l --format='%(contents)' "$PREV_TAG")
|
||||||
|
echo "📝 Previous changelog body: $PREV_CHANGELOG"
|
||||||
|
|
||||||
|
# Checking if tag message is the same as the changelog
|
||||||
|
if [[ "$PREV_CHANGELOG" == "$CHANGELOG_BODY" ]]; then
|
||||||
|
echo "❌ Changelog is the same as the previous changelog"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create and push tag
|
||||||
|
echo "🏷️ Creating tag: $TAG_NAME"
|
||||||
|
git tag "$TAG_NAME" -m "Release core v$VERSION" -m "$CHANGELOG_BODY"
|
||||||
|
git push origin "$TAG_NAME"
|
||||||
|
|
||||||
|
# Create GitHub release
|
||||||
|
TITLE="Core v$VERSION"
|
||||||
|
|
||||||
|
# Mark prereleases when version contains a hyphen
|
||||||
|
PRERELEASE_FLAG=""
|
||||||
|
if [[ "$VERSION" == *-* ]]; then
|
||||||
|
PRERELEASE_FLAG="--prerelease"
|
||||||
|
fi
|
||||||
|
|
||||||
|
LATEST_FLAG=""
|
||||||
|
if [[ "$VERSION" != *-* ]]; then
|
||||||
|
LATEST_FLAG="--latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
BODY="## Core Release v$VERSION
|
||||||
|
|
||||||
|
$CHANGELOG_BODY
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
go get github.com/maximhq/bifrost/core@v$VERSION
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
_This release was automatically created from version file: \`core/version\`_"
|
||||||
|
|
||||||
|
echo "🎉 Creating GitHub release for $TITLE..."
|
||||||
|
gh release create "$TAG_NAME" \
|
||||||
|
--title "$TITLE" \
|
||||||
|
--notes "$BODY" \
|
||||||
|
${PRERELEASE_FLAG} ${LATEST_FLAG}
|
||||||
|
|
||||||
|
echo "✅ Core released successfully"
|
||||||
|
echo "success=true" >> "$GITHUB_OUTPUT"
|
||||||
183
.github/workflows/scripts/release-framework.sh
vendored
Executable file
183
.github/workflows/scripts/release-framework.sh
vendored
Executable file
@@ -0,0 +1,183 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Release framework component
|
||||||
|
# Usage: ./release-framework.sh <version>
|
||||||
|
|
||||||
|
# Source Go utilities for exponential backoff
|
||||||
|
source "$(dirname "$0")/go-utils.sh"
|
||||||
|
|
||||||
|
# Making sure version is provided
|
||||||
|
if [ $# -ne 1 ]; then
|
||||||
|
echo "Usage: $0 <version>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION_RAW="$1"
|
||||||
|
# Ensure leading 'v' for module/tag semver
|
||||||
|
if [[ "$VERSION_RAW" == v* ]]; then
|
||||||
|
VERSION="$VERSION_RAW"
|
||||||
|
else
|
||||||
|
VERSION="v$VERSION_RAW"
|
||||||
|
fi
|
||||||
|
|
||||||
|
TAG_NAME="framework/${VERSION}"
|
||||||
|
|
||||||
|
echo "📦 Releasing framework $VERSION..."
|
||||||
|
|
||||||
|
# Ensure we have the latest version
|
||||||
|
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||||
|
if [ "$CURRENT_BRANCH" = "HEAD" ]; then
|
||||||
|
# In detached HEAD state (common in CI), use GITHUB_REF_NAME or default to main
|
||||||
|
CURRENT_BRANCH="${GITHUB_REF_NAME:-main}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Pulling latest changes from origin/$CURRENT_BRANCH..."
|
||||||
|
if ! git pull origin "$CURRENT_BRANCH"; then
|
||||||
|
echo "❌ Error: git pull origin $CURRENT_BRANCH failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for merge conflicts or unexpected working-tree changes
|
||||||
|
if ! git diff --quiet; then
|
||||||
|
echo "❌ Error: Unstaged changes detected after pull (possible merge conflict)"
|
||||||
|
git status --short
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git diff --cached --quiet; then
|
||||||
|
echo "❌ Error: Staged changes detected after pull (unexpected state)"
|
||||||
|
git status --short
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fetching all tags
|
||||||
|
git fetch --tags >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Get core version from version file
|
||||||
|
CORE_VERSION="v$(tr -d '\n\r' < core/version)"
|
||||||
|
|
||||||
|
|
||||||
|
# Before starting the test, we need to update hello-word plugin core dependencies
|
||||||
|
echo "🔧 Updating hello-word plugin core dependencies..."
|
||||||
|
cd examples/plugins/hello-world
|
||||||
|
go_get_with_backoff "github.com/maximhq/bifrost/core@$CORE_VERSION"
|
||||||
|
go mod tidy
|
||||||
|
git add go.mod go.sum
|
||||||
|
cd ../../..
|
||||||
|
|
||||||
|
echo "🔧 Using core version: $CORE_VERSION"
|
||||||
|
|
||||||
|
# Update framework dependencies
|
||||||
|
echo "🔧 Updating framework dependencies..."
|
||||||
|
cd framework
|
||||||
|
go_get_with_backoff "github.com/maximhq/bifrost/core@$CORE_VERSION"
|
||||||
|
go mod tidy
|
||||||
|
git add go.mod go.sum
|
||||||
|
|
||||||
|
# Check if there are any changes to commit
|
||||||
|
git add go.mod go.sum
|
||||||
|
|
||||||
|
|
||||||
|
# Validate framework build
|
||||||
|
echo "🔨 Validating framework build..."
|
||||||
|
go build ./...
|
||||||
|
cd ..
|
||||||
|
echo "✅ Framework build validation successful"
|
||||||
|
|
||||||
|
# Check if there are any changes to commit
|
||||||
|
if ! git diff --cached --quiet; then
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git commit -m "framework: bump core to $CORE_VERSION --skip-ci"
|
||||||
|
# Push the bump so go.mod/go.sum changes are recorded on the branch
|
||||||
|
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||||
|
if [ "$CURRENT_BRANCH" = "HEAD" ]; then
|
||||||
|
# In detached HEAD state (common in CI), use GITHUB_REF_NAME or default to main
|
||||||
|
CURRENT_BRANCH="${GITHUB_REF_NAME:-main}"
|
||||||
|
fi
|
||||||
|
git push origin "$CURRENT_BRANCH"
|
||||||
|
echo "🔧 Pushed framework bump to $CURRENT_BRANCH"
|
||||||
|
else
|
||||||
|
echo "No dependency changes detected; skipping commit."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Capturing changelog
|
||||||
|
CHANGELOG_BODY=$(cat framework/changelog.md)
|
||||||
|
# Skip comments from changelog
|
||||||
|
CHANGELOG_BODY=$(echo "$CHANGELOG_BODY" | grep -v '^<!--' | grep -v '^-->')
|
||||||
|
# If changelog is empty, return error
|
||||||
|
if [ -z "$CHANGELOG_BODY" ]; then
|
||||||
|
echo "❌ Changelog is empty"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "📝 New changelog: $CHANGELOG_BODY"
|
||||||
|
|
||||||
|
# Finding previous tag
|
||||||
|
echo "🔍 Finding previous tag..."
|
||||||
|
PREV_TAG=$(git tag -l "framework/v*" | sort -V | tail -1)
|
||||||
|
if [[ "$PREV_TAG" == "$TAG_NAME" ]]; then
|
||||||
|
PREV_TAG=$(git tag -l "framework/v*" | sort -V | tail -2 | head -1)
|
||||||
|
fi
|
||||||
|
echo "🔍 Previous tag: $PREV_TAG"
|
||||||
|
|
||||||
|
# Get message of the tag
|
||||||
|
echo "🔍 Getting previous tag message..."
|
||||||
|
PREV_CHANGELOG=$(git tag -l --format='%(contents)' "$PREV_TAG")
|
||||||
|
echo "📝 Previous changelog body: $PREV_CHANGELOG"
|
||||||
|
|
||||||
|
# Checking if tag message is the same as the changelog
|
||||||
|
if [[ "$PREV_CHANGELOG" == "$CHANGELOG_BODY" ]]; then
|
||||||
|
echo "❌ Changelog is the same as the previous changelog"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create and push tag
|
||||||
|
echo "🏷️ Creating tag: $TAG_NAME"
|
||||||
|
if git rev-parse --verify "$TAG_NAME" >/dev/null 2>&1; then
|
||||||
|
echo "Tag $TAG_NAME already exists; skipping tag creation."
|
||||||
|
else
|
||||||
|
git tag "$TAG_NAME" -m "Release framework $VERSION" -m "$CHANGELOG_BODY"
|
||||||
|
git push origin "$TAG_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create GitHub release
|
||||||
|
TITLE="Framework $VERSION"
|
||||||
|
|
||||||
|
# Mark prereleases when version contains a hyphen
|
||||||
|
PRERELEASE_FLAG=""
|
||||||
|
if [[ "$VERSION" == *-* ]]; then
|
||||||
|
PRERELEASE_FLAG="--prerelease"
|
||||||
|
fi
|
||||||
|
|
||||||
|
LATEST_FLAG=""
|
||||||
|
if [[ "$VERSION" != *-* ]]; then
|
||||||
|
LATEST_FLAG="--latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
BODY="## Framework Release $VERSION
|
||||||
|
|
||||||
|
$CHANGELOG_BODY
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
go get github.com/maximhq/bifrost/framework@$VERSION
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
_This release was automatically created and uses core version: \`$CORE_VERSION\`_"
|
||||||
|
|
||||||
|
echo "🎉 Creating GitHub release for $TITLE..."
|
||||||
|
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
|
||||||
|
echo "ℹ️ Release $TAG_NAME already exists. Skipping creation."
|
||||||
|
else
|
||||||
|
gh release create "$TAG_NAME" \
|
||||||
|
--title "$TITLE" \
|
||||||
|
--notes "$BODY" \
|
||||||
|
${PRERELEASE_FLAG} ${LATEST_FLAG}
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Framework released successfully"
|
||||||
|
echo "success=true" >> "$GITHUB_OUTPUT"
|
||||||
183
.github/workflows/scripts/release-single-plugin.sh
vendored
Executable file
183
.github/workflows/scripts/release-single-plugin.sh
vendored
Executable file
@@ -0,0 +1,183 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Release a single plugin
|
||||||
|
# Usage: ./release-single-plugin.sh <plugin-name> [core-version] [framework-version]
|
||||||
|
|
||||||
|
# Source Go utilities for exponential backoff
|
||||||
|
source "$(dirname "$0")/go-utils.sh"
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
echo "Usage: $0 <plugin-name> [core-version] [framework-version]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PLUGIN_NAME="$1"
|
||||||
|
|
||||||
|
# Get core version from parameter or version file
|
||||||
|
if [ -n "${2:-}" ]; then
|
||||||
|
CORE_VERSION="$2"
|
||||||
|
else
|
||||||
|
CORE_VERSION="v$(tr -d '\n\r' < core/version)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get framework version from parameter or version file
|
||||||
|
if [ -n "${3:-}" ]; then
|
||||||
|
FRAMEWORK_VERSION="$3"
|
||||||
|
else
|
||||||
|
FRAMEWORK_VERSION="v$(tr -d '\n\r' < framework/version)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure we have the latest version
|
||||||
|
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||||
|
if [ "$CURRENT_BRANCH" = "HEAD" ]; then
|
||||||
|
# In detached HEAD state (common in CI), use GITHUB_REF_NAME or default to main
|
||||||
|
CURRENT_BRANCH="${GITHUB_REF_NAME:-main}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Pulling latest changes from origin/$CURRENT_BRANCH..."
|
||||||
|
if ! git pull origin "$CURRENT_BRANCH"; then
|
||||||
|
echo "❌ Error: git pull origin $CURRENT_BRANCH failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔌 Releasing plugin: $PLUGIN_NAME"
|
||||||
|
echo "🔧 Core version: $CORE_VERSION"
|
||||||
|
echo "🔧 Framework version: $FRAMEWORK_VERSION"
|
||||||
|
|
||||||
|
PLUGIN_DIR="plugins/$PLUGIN_NAME"
|
||||||
|
VERSION_FILE="$PLUGIN_DIR/version"
|
||||||
|
|
||||||
|
if [ ! -f "$VERSION_FILE" ]; then
|
||||||
|
echo "❌ Version file not found: $VERSION_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PLUGIN_VERSION=$(tr -d '\n\r' < "$VERSION_FILE")
|
||||||
|
TAG_NAME="plugins/${PLUGIN_NAME}/v${PLUGIN_VERSION}"
|
||||||
|
|
||||||
|
echo "📦 Plugin version: $PLUGIN_VERSION"
|
||||||
|
echo "🏷️ Tag name: $TAG_NAME"
|
||||||
|
|
||||||
|
|
||||||
|
# Update plugin dependencies
|
||||||
|
echo "🔧 Updating plugin dependencies..."
|
||||||
|
cd "$PLUGIN_DIR"
|
||||||
|
|
||||||
|
# Update core dependency
|
||||||
|
if [ -f "go.mod" ]; then
|
||||||
|
go_get_with_backoff "github.com/maximhq/bifrost/core@${CORE_VERSION}"
|
||||||
|
go_get_with_backoff "github.com/maximhq/bifrost/framework@${FRAMEWORK_VERSION}"
|
||||||
|
go mod tidy
|
||||||
|
git add go.mod go.sum || true
|
||||||
|
|
||||||
|
# Validate build
|
||||||
|
echo "🔨 Validating plugin build..."
|
||||||
|
go build ./...
|
||||||
|
echo "✅ Plugin $PLUGIN_NAME build validation successful"
|
||||||
|
else
|
||||||
|
echo "ℹ️ No go.mod found, skipping Go dependency update"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
# Commit and push changes if any
|
||||||
|
if ! git diff --cached --quiet; then
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
echo "🔧 Committing and pushing changes..."
|
||||||
|
git commit -m "plugins/${PLUGIN_NAME}: bump core to $CORE_VERSION and framework to $FRAMEWORK_VERSION --skip-ci"
|
||||||
|
git push -u origin HEAD
|
||||||
|
else
|
||||||
|
echo "ℹ️ No staged changes to commit"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Capturing changelog
|
||||||
|
CHANGELOG_BODY=$(cat $PLUGIN_DIR/changelog.md)
|
||||||
|
# Skip comments from changelog
|
||||||
|
CHANGELOG_BODY=$(echo "$CHANGELOG_BODY" | grep -v '^<!--' | grep -v '^-->' || true)
|
||||||
|
# If changelog is empty, return error
|
||||||
|
if [ -z "$CHANGELOG_BODY" ]; then
|
||||||
|
echo "❌ Changelog is empty"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "📝 New changelog: $CHANGELOG_BODY"
|
||||||
|
|
||||||
|
# Finding previous tag
|
||||||
|
echo "🔍 Finding previous tag..."
|
||||||
|
PREV_TAG=$(git tag -l "plugins/${PLUGIN_NAME}/v*" | sort -V | tail -1)
|
||||||
|
if [[ "$PREV_TAG" == "$TAG_NAME" ]]; then
|
||||||
|
PREV_TAG=$(git tag -l "plugins/${PLUGIN_NAME}/v*" | sort -V | tail -2 | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Only validate changelog changes if there's a previous tag
|
||||||
|
if [ -n "$PREV_TAG" ]; then
|
||||||
|
echo "🔍 Previous tag: $PREV_TAG"
|
||||||
|
|
||||||
|
# Get message of the tag
|
||||||
|
echo "🔍 Getting previous tag message..."
|
||||||
|
PREV_CHANGELOG=$(git tag -l --format='%(contents)' "$PREV_TAG")
|
||||||
|
echo "📝 Previous changelog body: $PREV_CHANGELOG"
|
||||||
|
|
||||||
|
# Checking if tag message is the same as the changelog
|
||||||
|
if [[ "$PREV_CHANGELOG" == "$CHANGELOG_BODY" ]]; then
|
||||||
|
echo "❌ Changelog is the same as the previous changelog"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "ℹ️ No previous tag found - this is the first release"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Create and push tag
|
||||||
|
echo "🏷️ Creating tag: $TAG_NAME"
|
||||||
|
|
||||||
|
if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then
|
||||||
|
echo "ℹ️ Tag already exists: $TAG_NAME (skipping creation)"
|
||||||
|
else
|
||||||
|
git tag "$TAG_NAME" -m "Release plugin $PLUGIN_NAME v$PLUGIN_VERSION" -m "$CHANGELOG_BODY"
|
||||||
|
git push origin "$TAG_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create GitHub release
|
||||||
|
TITLE="Plugin $PLUGIN_NAME v$PLUGIN_VERSION"
|
||||||
|
|
||||||
|
# Mark prereleases when version contains a hyphen
|
||||||
|
PRERELEASE_FLAG=""
|
||||||
|
if [[ "$PLUGIN_VERSION" == *-* ]]; then
|
||||||
|
PRERELEASE_FLAG="--prerelease"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Mark as latest if not a prerelease
|
||||||
|
LATEST_FLAG=""
|
||||||
|
if [[ "$PLUGIN_VERSION" != *-* ]]; then
|
||||||
|
LATEST_FLAG="--latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
BODY="## Plugin Release: $PLUGIN_NAME v$PLUGIN_VERSION
|
||||||
|
|
||||||
|
$CHANGELOG_BODY
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Update your go.mod to use the new plugin version
|
||||||
|
go get github.com/maximhq/bifrost/plugins/$PLUGIN_NAME@v$PLUGIN_VERSION
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
_This release was automatically created from version file: \`plugins/$PLUGIN_NAME/version\`_"
|
||||||
|
|
||||||
|
echo "🎉 Creating GitHub release for $TITLE..."
|
||||||
|
|
||||||
|
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
|
||||||
|
echo "ℹ️ Release $TAG_NAME already exists. Skipping creation."
|
||||||
|
else
|
||||||
|
gh release create "$TAG_NAME" \
|
||||||
|
--title "$TITLE" \
|
||||||
|
--notes "$BODY" \
|
||||||
|
${PRERELEASE_FLAG} ${LATEST_FLAG}
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Plugin $PLUGIN_NAME released successfully"
|
||||||
|
echo "success=true" >> "${GITHUB_OUTPUT:-/dev/null}"
|
||||||
77
.github/workflows/scripts/revert-latest.sh
vendored
Executable file
77
.github/workflows/scripts/revert-latest.sh
vendored
Executable file
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Overwrite latest with a specific version from R2
|
||||||
|
# Usage: ./revert-latest.sh <version>
|
||||||
|
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
echo "Usage: $0 <version> (e.g., v1.2.3)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="$1"
|
||||||
|
# Ensure version starts with 'v'
|
||||||
|
if [[ ! "$VERSION" =~ ^v ]]; then
|
||||||
|
VERSION="v${VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate required environment variables
|
||||||
|
: "${R2_ENDPOINT:?R2_ENDPOINT env var is required}"
|
||||||
|
: "${R2_BUCKET:?R2_BUCKET env var is required}"
|
||||||
|
|
||||||
|
# Clean endpoint URL
|
||||||
|
R2_ENDPOINT="$(echo "$R2_ENDPOINT" | tr -d '[:space:]')"
|
||||||
|
|
||||||
|
echo "🔄 Reverting latest to version: $VERSION"
|
||||||
|
|
||||||
|
# Function to sync with retry logic
|
||||||
|
sync_with_retry() {
|
||||||
|
local source_path="$1"
|
||||||
|
local dest_path="$2"
|
||||||
|
local max_retries=3
|
||||||
|
|
||||||
|
for attempt in $(seq 1 $max_retries); do
|
||||||
|
echo "🔄 Attempt $attempt/$max_retries: Syncing $source_path to $dest_path"
|
||||||
|
|
||||||
|
if aws s3 sync "$source_path" "$dest_path" \
|
||||||
|
--endpoint-url "$R2_ENDPOINT" \
|
||||||
|
--profile "${R2_AWS_PROFILE:-R2}" \
|
||||||
|
--no-progress \
|
||||||
|
--delete; then
|
||||||
|
echo "✅ Sync successful from $source_path to $dest_path"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "⚠️ Attempt $attempt failed"
|
||||||
|
if [ $attempt -lt $max_retries ]; then
|
||||||
|
delay=$((2 ** attempt))
|
||||||
|
echo "🕐 Waiting ${delay}s before retry..."
|
||||||
|
sleep $delay
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "❌ All $max_retries attempts failed for syncing to $dest_path"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if the version exists in R2
|
||||||
|
echo "🔍 Checking if version $VERSION exists..."
|
||||||
|
if ! aws s3 ls "s3://$R2_BUCKET/bifrost/$VERSION/" \
|
||||||
|
--endpoint-url "$R2_ENDPOINT" \
|
||||||
|
--profile "${R2_AWS_PROFILE:-R2}" >/dev/null 2>&1; then
|
||||||
|
echo "❌ Version $VERSION not found in R2 bucket"
|
||||||
|
echo "Available versions:"
|
||||||
|
aws s3 ls "s3://$R2_BUCKET/bifrost/" \
|
||||||
|
--endpoint-url "$R2_ENDPOINT" \
|
||||||
|
--profile "${R2_AWS_PROFILE:-R2}" | grep "PRE v" | awk '{print $2}' | sed 's/\///g' || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Version $VERSION found in R2"
|
||||||
|
|
||||||
|
# Sync the specific version to latest
|
||||||
|
if ! sync_with_retry "s3://$R2_BUCKET/bifrost/$VERSION/" "s3://$R2_BUCKET/bifrost/latest/"; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🎉 Successfully reverted latest to version $VERSION"
|
||||||
199
.github/workflows/scripts/run-governance-e2e-tests.sh
vendored
Executable file
199
.github/workflows/scripts/run-governance-e2e-tests.sh
vendored
Executable file
@@ -0,0 +1,199 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Run Governance E2E Tests
|
||||||
|
# This script builds Bifrost, starts it with the governance test config,
|
||||||
|
# runs the governance tests, and cleans up.
|
||||||
|
#
|
||||||
|
# Usage: ./run-governance-e2e-tests.sh
|
||||||
|
|
||||||
|
echo "🛡️ Starting Governance E2E Tests..."
|
||||||
|
|
||||||
|
# Get the root directory of the repo
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
BIFROST_PORT=8080
|
||||||
|
BIFROST_HOST="localhost"
|
||||||
|
BIFROST_URL="http://${BIFROST_HOST}:${BIFROST_PORT}"
|
||||||
|
APP_DIR="tests/governance"
|
||||||
|
BIFROST_BINARY="tmp/bifrost-http"
|
||||||
|
BIFROST_PID_FILE="/tmp/bifrost-governance-test.pid"
|
||||||
|
BIFROST_LOG_FILE="/tmp/bifrost-governance-test.log"
|
||||||
|
MAX_STARTUP_WAIT=30 # seconds
|
||||||
|
|
||||||
|
# Cleanup function to ensure Bifrost is stopped
|
||||||
|
cleanup() {
|
||||||
|
local exit_code=$?
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}🧹 Cleaning up...${NC}"
|
||||||
|
|
||||||
|
# Stop Bifrost if running
|
||||||
|
if [ -f "$BIFROST_PID_FILE" ]; then
|
||||||
|
BIFROST_PID=$(cat "$BIFROST_PID_FILE")
|
||||||
|
if ps -p "$BIFROST_PID" > /dev/null 2>&1; then
|
||||||
|
echo -e "${CYAN}Stopping Bifrost (PID: $BIFROST_PID)...${NC}"
|
||||||
|
kill "$BIFROST_PID" 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
# Force kill if still running
|
||||||
|
if ps -p "$BIFROST_PID" > /dev/null 2>&1; then
|
||||||
|
echo -e "${YELLOW}Force killing Bifrost...${NC}"
|
||||||
|
kill -9 "$BIFROST_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
rm -f "$BIFROST_PID_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up log file
|
||||||
|
if [ -f "$BIFROST_LOG_FILE" ]; then
|
||||||
|
echo -e "${CYAN}Bifrost logs saved to: $BIFROST_LOG_FILE${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up test database
|
||||||
|
if [ -f "data/governance-test.db" ]; then
|
||||||
|
echo -e "${CYAN}Cleaning up test database...${NC}"
|
||||||
|
rm -f "data/governance-test.db"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $exit_code -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}✅ Cleanup complete${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Cleanup complete (tests failed)${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $exit_code
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set up trap to cleanup on exit
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
# Step 1: Validate prerequisites
|
||||||
|
echo -e "${CYAN}📋 Step 1: Validating prerequisites...${NC}"
|
||||||
|
|
||||||
|
if [ ! -d "$APP_DIR" ]; then
|
||||||
|
echo -e "${RED}❌ App directory not found: $APP_DIR${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$APP_DIR/config.json" ]; then
|
||||||
|
echo -e "${RED}❌ Config file not found: $APP_DIR/config.json${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check required environment variables (OPENAI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY)
|
||||||
|
if [ -z "${OPENAI_API_KEY:-}" ] || [ -z "${ANTHROPIC_API_KEY:-}" ] || [ -z "${OPENROUTER_API_KEY:-}" ]; then
|
||||||
|
echo -e "${RED}❌ Required environment variables are not set${NC}"
|
||||||
|
echo -e "${YELLOW}Set them with: export OPENAI_API_KEY='sk-...'${NC}"
|
||||||
|
echo -e "${YELLOW}Set them with: export ANTHROPIC_API_KEY='sk-...'${NC}"
|
||||||
|
echo -e "${YELLOW}Set them with: export OPENROUTER_API_KEY='sk-...'${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}✅ Prerequisites validated${NC}"
|
||||||
|
|
||||||
|
# Step 2: Build Bifrost
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}📦 Step 2: Building Bifrost...${NC}"
|
||||||
|
|
||||||
|
# Use make to build with LOCAL=1 to use the workspace (go.work)
|
||||||
|
# This ensures we test the local governance plugin code, not the published version
|
||||||
|
if ! make build LOCAL=1; then
|
||||||
|
echo -e "${RED}❌ Failed to build Bifrost${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$BIFROST_BINARY" ]; then
|
||||||
|
echo -e "${RED}❌ Bifrost binary not found at: $BIFROST_BINARY${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ Bifrost built successfully${NC}"
|
||||||
|
|
||||||
|
# Step 3: Start Bifrost in background
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}🚀 Step 3: Starting Bifrost server...${NC}"
|
||||||
|
|
||||||
|
# Ensure data directory exists for SQLite database
|
||||||
|
mkdir -p data
|
||||||
|
|
||||||
|
# Start Bifrost in background
|
||||||
|
echo -e "${YELLOW}Starting Bifrost on ${BIFROST_URL}...${NC}"
|
||||||
|
"$BIFROST_BINARY" -app-dir "$APP_DIR" -port "$BIFROST_PORT" -host "$BIFROST_HOST" > "$BIFROST_LOG_FILE" 2>&1 &
|
||||||
|
BIFROST_PID=$!
|
||||||
|
echo "$BIFROST_PID" > "$BIFROST_PID_FILE"
|
||||||
|
|
||||||
|
echo -e "${CYAN}Bifrost started with PID: $BIFROST_PID${NC}"
|
||||||
|
|
||||||
|
# Step 4: Wait for Bifrost to be ready
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}⏳ Step 4: Waiting for Bifrost to be ready...${NC}"
|
||||||
|
|
||||||
|
WAIT_COUNT=0
|
||||||
|
until curl -sf "${BIFROST_URL}/health" > /dev/null 2>&1; do
|
||||||
|
if [ $WAIT_COUNT -ge $MAX_STARTUP_WAIT ]; then
|
||||||
|
echo -e "${RED}❌ Bifrost failed to start within ${MAX_STARTUP_WAIT} seconds${NC}"
|
||||||
|
echo -e "${YELLOW}Last 50 lines of Bifrost logs:${NC}"
|
||||||
|
tail -n 50 "$BIFROST_LOG_FILE" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if process is still running
|
||||||
|
if ! ps -p "$BIFROST_PID" > /dev/null 2>&1; then
|
||||||
|
echo -e "${RED}❌ Bifrost process died${NC}"
|
||||||
|
echo -e "${YELLOW}Bifrost logs:${NC}"
|
||||||
|
cat "$BIFROST_LOG_FILE" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
WAIT_COUNT=$((WAIT_COUNT + 1))
|
||||||
|
echo -e "${YELLOW}Waiting for Bifrost... ($WAIT_COUNT/${MAX_STARTUP_WAIT})${NC}"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ Bifrost is ready and responding${NC}"
|
||||||
|
|
||||||
|
# Step 5: Run governance tests
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}🧪 Step 5: Running governance tests...${NC}"
|
||||||
|
|
||||||
|
cd tests/governance
|
||||||
|
|
||||||
|
# Run tests with go test (disable workspace to avoid module conflicts)
|
||||||
|
echo -e "${YELLOW}Running go test in tests/governance...${NC}"
|
||||||
|
|
||||||
|
# Run tests with verbose output and timeout
|
||||||
|
# Use GOWORK=off to disable the workspace file and test the module independently
|
||||||
|
# Use -count=1 to disable test cache
|
||||||
|
GOWORK=off go test -v -timeout 10m -count=1 ./...
|
||||||
|
TEST_EXIT_CODE=$?
|
||||||
|
if [ $TEST_EXIT_CODE -ne 0 ]; then
|
||||||
|
echo -e "${RED}❌ Governance tests failed (exit code: $TEST_EXIT_CODE)${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✅ All governance tests passed${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
# Step 6: Report results
|
||||||
|
echo ""
|
||||||
|
if [ $TEST_EXIT_CODE -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}═══════════════════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${GREEN}✅ Governance E2E Tests PASSED${NC}"
|
||||||
|
echo -e "${GREEN}═══════════════════════════════════════════════════════${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}═══════════════════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${RED}❌ Governance E2E Tests FAILED${NC}"
|
||||||
|
echo -e "${RED}═══════════════════════════════════════════════════════${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Check logs at: $BIFROST_LOG_FILE${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $TEST_EXIT_CODE
|
||||||
299
.github/workflows/scripts/run-integration-tests.sh
vendored
Executable file
299
.github/workflows/scripts/run-integration-tests.sh
vendored
Executable file
@@ -0,0 +1,299 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Run integration tests with Bifrost binary and PostgreSQL
|
||||||
|
# Usage: ./run-integration-tests.sh <bifrost-binary-path> [port]
|
||||||
|
|
||||||
|
# Get the absolute path of the script directory
|
||||||
|
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||||
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
|
else
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Repository root (3 levels up from .github/workflows/scripts)
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd -P)"
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
if [ "${1:-}" = "" ]; then
|
||||||
|
echo "Usage: $0 <bifrost-binary-path> [port]" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "Arguments:" >&2
|
||||||
|
echo " bifrost-binary-path Path to the bifrost-http binary" >&2
|
||||||
|
echo " port Port to run Bifrost on (default: 8080)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BIFROST_BINARY="$1"
|
||||||
|
PORT="${2:-8080}"
|
||||||
|
|
||||||
|
# PostgreSQL configuration (from environment or defaults)
|
||||||
|
POSTGRES_HOST="${POSTGRES_HOST:-localhost}"
|
||||||
|
POSTGRES_PORT="${POSTGRES_PORT:-5432}"
|
||||||
|
POSTGRES_USER="${POSTGRES_USER:-bifrost}"
|
||||||
|
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-bifrost_password}"
|
||||||
|
POSTGRES_DB="${POSTGRES_DB:-bifrost}"
|
||||||
|
POSTGRES_SSLMODE="${POSTGRES_SSLMODE:-disable}"
|
||||||
|
|
||||||
|
# Validate binary exists and is executable
|
||||||
|
if [ ! -f "$BIFROST_BINARY" ]; then
|
||||||
|
echo "❌ Error: Bifrost binary not found: $BIFROST_BINARY" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$BIFROST_BINARY" ]; then
|
||||||
|
echo "❌ Error: Bifrost binary is not executable: $BIFROST_BINARY" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🧪 Running Bifrost Integration Tests"
|
||||||
|
echo " Binary: $BIFROST_BINARY"
|
||||||
|
echo " Port: $PORT"
|
||||||
|
|
||||||
|
# Create temp directory for merged config
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
MERGED_CONFIG="$TEMP_DIR/config.json"
|
||||||
|
echo "📁 Using temp directory: $TEMP_DIR"
|
||||||
|
|
||||||
|
# Cleanup function
|
||||||
|
cleanup() {
|
||||||
|
local exit_code=$?
|
||||||
|
echo ""
|
||||||
|
echo "🧹 Cleaning up..."
|
||||||
|
|
||||||
|
# Kill Bifrost server if running
|
||||||
|
if [ -n "${BIFROST_PID:-}" ]; then
|
||||||
|
echo " Stopping Bifrost server (PID: $BIFROST_PID)..."
|
||||||
|
kill "$BIFROST_PID" 2>/dev/null || true
|
||||||
|
wait "$BIFROST_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove temp directory
|
||||||
|
if [ -d "$TEMP_DIR" ]; then
|
||||||
|
echo " Removing temp directory..."
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $exit_code
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Create merged config
|
||||||
|
echo "📝 Creating merged config with PostgreSQL..."
|
||||||
|
|
||||||
|
# Base config from tests/integrations
|
||||||
|
BASE_CONFIG="$REPO_ROOT/tests/integrations/config.json"
|
||||||
|
|
||||||
|
if [ ! -f "$BASE_CONFIG" ]; then
|
||||||
|
echo "❌ Error: Base config not found: $BASE_CONFIG" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use jq to merge configs if available, otherwise use Python
|
||||||
|
#
|
||||||
|
# NOTE: The following config merge INTENTIONALLY OVERWRITES any existing
|
||||||
|
# config_store and logs_store keys from the base config. This is required
|
||||||
|
# because:
|
||||||
|
# 1. Integration tests MUST use the local PostgreSQL instance to validate
|
||||||
|
# database-related functionality (config persistence, logging, etc.)
|
||||||
|
# 2. The base config (tests/integrations/config.json) typically has these
|
||||||
|
# stores disabled; we need to fully replace them with enabled PostgreSQL
|
||||||
|
# config pointing to the test container.
|
||||||
|
# 3. Deep-merging is NOT desired here - we need a complete, known-good
|
||||||
|
# PostgreSQL configuration regardless of what the base config contains.
|
||||||
|
#
|
||||||
|
# Edge cases handled:
|
||||||
|
# - Base config has no store keys: jq/Python adds them (no issue)
|
||||||
|
# - Base config has stores disabled: fully replaced with enabled PostgreSQL
|
||||||
|
# - Base config has different store type (e.g., sqlite): fully replaced
|
||||||
|
# - Base config has partial PostgreSQL config: fully replaced to ensure
|
||||||
|
# correct credentials for the test container
|
||||||
|
#
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
# jq '. + {...}' performs shallow merge at top level, fully replacing
|
||||||
|
# config_store and logs_store keys (intentional - see note above)
|
||||||
|
jq --arg host "$POSTGRES_HOST" \
|
||||||
|
--arg port "$POSTGRES_PORT" \
|
||||||
|
--arg user "$POSTGRES_USER" \
|
||||||
|
--arg pass "$POSTGRES_PASSWORD" \
|
||||||
|
--arg db "$POSTGRES_DB" \
|
||||||
|
--arg ssl "$POSTGRES_SSLMODE" \
|
||||||
|
'. + {
|
||||||
|
"config_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "postgres",
|
||||||
|
"config": {
|
||||||
|
"host": $host,
|
||||||
|
"port": $port,
|
||||||
|
"user": $user,
|
||||||
|
"password": $pass,
|
||||||
|
"db_name": $db,
|
||||||
|
"ssl_mode": $ssl
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logs_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "postgres",
|
||||||
|
"config": {
|
||||||
|
"host": $host,
|
||||||
|
"port": $port,
|
||||||
|
"user": $user,
|
||||||
|
"password": $pass,
|
||||||
|
"db_name": $db,
|
||||||
|
"ssl_mode": $ssl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' "$BASE_CONFIG" > "$MERGED_CONFIG"
|
||||||
|
else
|
||||||
|
# Fallback to Python if jq is not available
|
||||||
|
# Same intentional overwrite behavior as jq path (see note above)
|
||||||
|
python3 - "$BASE_CONFIG" "$MERGED_CONFIG" << 'EOF'
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
base_path = sys.argv[1]
|
||||||
|
merged_path = sys.argv[2]
|
||||||
|
|
||||||
|
with open(base_path, "r") as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
postgres_config = {
|
||||||
|
"host": os.environ.get("POSTGRES_HOST", "localhost"),
|
||||||
|
"port": os.environ.get("POSTGRES_PORT", "5432"),
|
||||||
|
"user": os.environ.get("POSTGRES_USER", "bifrost"),
|
||||||
|
"password": os.environ.get("POSTGRES_PASSWORD", "bifrost_password"),
|
||||||
|
"db_name": os.environ.get("POSTGRES_DB", "bifrost"),
|
||||||
|
"ssl_mode": os.environ.get("POSTGRES_SSLMODE", "disable")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Intentionally overwrite any existing store config to force PostgreSQL
|
||||||
|
# for integration tests (see detailed note in bash section above)
|
||||||
|
config["config_store"] = {
|
||||||
|
"enabled": True,
|
||||||
|
"type": "postgres",
|
||||||
|
"config": postgres_config
|
||||||
|
}
|
||||||
|
|
||||||
|
config["logs_store"] = {
|
||||||
|
"enabled": True,
|
||||||
|
"type": "postgres",
|
||||||
|
"config": postgres_config.copy()
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(merged_path, "w") as f:
|
||||||
|
json.dump(config, f, indent=2)
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " ✅ Merged config created at: $MERGED_CONFIG"
|
||||||
|
|
||||||
|
# Reset PostgreSQL database
|
||||||
|
echo "🔄 Resetting PostgreSQL database..."
|
||||||
|
DOCKER_COMPOSE_FILE="$REPO_ROOT/.github/workflows/configs/docker-compose.yml"
|
||||||
|
|
||||||
|
if [ -f "$DOCKER_COMPOSE_FILE" ]; then
|
||||||
|
POSTGRES_CONTAINER=$(docker compose -f "$DOCKER_COMPOSE_FILE" ps -q postgres 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$POSTGRES_CONTAINER" ]; then
|
||||||
|
docker exec "$POSTGRES_CONTAINER" \
|
||||||
|
psql -U "$POSTGRES_USER" -d postgres -c "DROP DATABASE IF EXISTS $POSTGRES_DB; CREATE DATABASE $POSTGRES_DB;" \
|
||||||
|
2>/dev/null || echo " ⚠️ Could not reset database (container may not be running)"
|
||||||
|
echo " ✅ Database reset complete"
|
||||||
|
else
|
||||||
|
echo " ⚠️ PostgreSQL container not found, skipping database reset"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ⚠️ Docker compose file not found, skipping database reset"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start Bifrost server
|
||||||
|
echo "🚀 Starting Bifrost server..."
|
||||||
|
SERVER_LOG="$TEMP_DIR/server.log"
|
||||||
|
|
||||||
|
"$BIFROST_BINARY" --app-dir "$TEMP_DIR" --port "$PORT" --log-level debug > "$SERVER_LOG" 2>&1 &
|
||||||
|
BIFROST_PID=$!
|
||||||
|
|
||||||
|
echo " Started Bifrost with PID: $BIFROST_PID"
|
||||||
|
|
||||||
|
# Wait for server to be ready
|
||||||
|
echo "⏳ Waiting for Bifrost to start..."
|
||||||
|
MAX_WAIT=60
|
||||||
|
ELAPSED=0
|
||||||
|
SERVER_READY=false
|
||||||
|
|
||||||
|
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||||
|
if grep -q "successfully started bifrost" "$SERVER_LOG" 2>/dev/null; then
|
||||||
|
SERVER_READY=true
|
||||||
|
echo " ✅ Bifrost started successfully"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if server process is still running
|
||||||
|
if ! kill -0 "$BIFROST_PID" 2>/dev/null; then
|
||||||
|
echo " ❌ Bifrost process died unexpectedly"
|
||||||
|
echo " Server log:"
|
||||||
|
cat "$SERVER_LOG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
ELAPSED=$((ELAPSED + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$SERVER_READY" = false ]; then
|
||||||
|
echo " ❌ Bifrost failed to start within ${MAX_WAIT}s"
|
||||||
|
echo " Server log:"
|
||||||
|
cat "$SERVER_LOG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set environment variable for tests
|
||||||
|
export BIFROST_BASE_URL="http://localhost:$PORT"
|
||||||
|
echo " BIFROST_BASE_URL=$BIFROST_BASE_URL"
|
||||||
|
|
||||||
|
# Run Python integration tests
|
||||||
|
echo ""
|
||||||
|
echo "🧪 Running Python integration tests..."
|
||||||
|
echo "="
|
||||||
|
|
||||||
|
cd "$REPO_ROOT/tests/integrations"
|
||||||
|
|
||||||
|
# Check if uv is available
|
||||||
|
if command -v uv >/dev/null 2>&1; then
|
||||||
|
echo "📦 Installing dependencies with uv..."
|
||||||
|
uv sync --frozen --quiet
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🏃 Running tests..."
|
||||||
|
TEST_EXIT_CODE=0
|
||||||
|
uv run python run_all_tests.py --verbose || TEST_EXIT_CODE=$?
|
||||||
|
else
|
||||||
|
echo "⚠️ uv not found, trying pip..."
|
||||||
|
|
||||||
|
# Create virtual environment if needed
|
||||||
|
if [ ! -d ".venv" ]; then
|
||||||
|
python3 -m venv .venv
|
||||||
|
fi
|
||||||
|
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -q -e .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🏃 Running tests..."
|
||||||
|
TEST_EXIT_CODE=0
|
||||||
|
python run_all_tests.py --verbose || TEST_EXIT_CODE=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "="
|
||||||
|
|
||||||
|
if [ $TEST_EXIT_CODE -eq 0 ]; then
|
||||||
|
echo "✅ All integration tests passed!"
|
||||||
|
else
|
||||||
|
echo "❌ Some integration tests failed (exit code: $TEST_EXIT_CODE)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Exit with test result code (cleanup trap will run)
|
||||||
|
exit $TEST_EXIT_CODE
|
||||||
|
|
||||||
4226
.github/workflows/scripts/run-migration-tests.sh
vendored
Executable file
4226
.github/workflows/scripts/run-migration-tests.sh
vendored
Executable file
File diff suppressed because it is too large
Load Diff
172
.github/workflows/scripts/run-tests.sh
vendored
Executable file
172
.github/workflows/scripts/run-tests.sh
vendored
Executable file
@@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Comprehensive test runner for Bifrost PR validation
|
||||||
|
# This script runs all test suites to validate changes
|
||||||
|
|
||||||
|
echo "🧪 Starting Bifrost Test Suite..."
|
||||||
|
echo "=================================="
|
||||||
|
|
||||||
|
# Color codes for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Track test results
|
||||||
|
TESTS_PASSED=0
|
||||||
|
TESTS_FAILED=0
|
||||||
|
|
||||||
|
# Function to report test result
|
||||||
|
report_result() {
|
||||||
|
local test_name=$1
|
||||||
|
local result=$2
|
||||||
|
|
||||||
|
if [ "$result" -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}✅ $test_name passed${NC}"
|
||||||
|
((TESTS_PASSED++))
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ $test_name failed${NC}"
|
||||||
|
((TESTS_FAILED++))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. Core Build Validation
|
||||||
|
echo ""
|
||||||
|
echo "📦 1/5 - Validating Core Build..."
|
||||||
|
echo "-----------------------------------"
|
||||||
|
cd core
|
||||||
|
if go mod download && go build ./...; then
|
||||||
|
report_result "Core Build" 0
|
||||||
|
else
|
||||||
|
report_result "Core Build" 1
|
||||||
|
fi
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# 2. Build MCP Test Servers
|
||||||
|
echo ""
|
||||||
|
echo "🔌 2/5 - Building MCP Test Servers..."
|
||||||
|
echo "-----------------------------------"
|
||||||
|
MCP_BUILD_FAILED=0
|
||||||
|
for mcp_dir in examples/mcps/*/; do
|
||||||
|
if [ -d "$mcp_dir" ]; then
|
||||||
|
mcp_name=$(basename "$mcp_dir")
|
||||||
|
if [ -f "$mcp_dir/go.mod" ]; then
|
||||||
|
echo " Building $mcp_name (Go)..."
|
||||||
|
mkdir -p "$mcp_dir/bin"
|
||||||
|
if cd "$mcp_dir" && GOWORK=off go build -o "bin/$mcp_name" . && cd - > /dev/null; then
|
||||||
|
echo -e " ${GREEN}✓ $mcp_name${NC}"
|
||||||
|
else
|
||||||
|
echo -e " ${RED}✗ $mcp_name${NC}"
|
||||||
|
MCP_BUILD_FAILED=1
|
||||||
|
cd - > /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
elif [ -f "$mcp_dir/package.json" ]; then
|
||||||
|
echo " Building $mcp_name (TypeScript)..."
|
||||||
|
if cd "$mcp_dir" && npm install --silent && npm run build && cd - > /dev/null; then
|
||||||
|
echo -e " ${GREEN}✓ $mcp_name${NC}"
|
||||||
|
else
|
||||||
|
echo -e " ${RED}✗ $mcp_name${NC}"
|
||||||
|
MCP_BUILD_FAILED=1
|
||||||
|
cd - > /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
report_result "MCP Test Servers Build" $MCP_BUILD_FAILED
|
||||||
|
|
||||||
|
# 3. Core Provider Tests
|
||||||
|
echo ""
|
||||||
|
echo "🔧 3/5 - Running Core Provider Tests..."
|
||||||
|
echo "-----------------------------------"
|
||||||
|
cd core
|
||||||
|
if go test -v -run . ./...; then
|
||||||
|
report_result "Core Provider Tests" 0
|
||||||
|
else
|
||||||
|
report_result "Core Provider Tests" 1
|
||||||
|
fi
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# 4. Governance Tests
|
||||||
|
echo ""
|
||||||
|
echo "🛡️ 4/5 - Running Governance Tests..."
|
||||||
|
echo "-----------------------------------"
|
||||||
|
if [ -d "tests/governance" ]; then
|
||||||
|
cd tests/governance
|
||||||
|
|
||||||
|
# Check if virtual environment exists, create if not
|
||||||
|
if [ ! -d "venv" ]; then
|
||||||
|
echo "Creating Python virtual environment..."
|
||||||
|
python3 -m venv venv
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Activate virtual environment
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
echo "Installing Python dependencies..."
|
||||||
|
pip install -q -r requirements.txt
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
if pytest -v; then
|
||||||
|
report_result "Governance Tests" 0
|
||||||
|
else
|
||||||
|
report_result "Governance Tests" 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
deactivate
|
||||||
|
cd ../..
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠️ Governance tests directory not found, skipping...${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Integration Tests
|
||||||
|
echo ""
|
||||||
|
echo "🔗 5/5 - Running Integration Tests..."
|
||||||
|
echo "-----------------------------------"
|
||||||
|
if [ -d "tests/integrations" ]; then
|
||||||
|
cd tests/integrations
|
||||||
|
|
||||||
|
# Check if virtual environment exists, create if not
|
||||||
|
if [ ! -d "venv" ]; then
|
||||||
|
echo "Creating Python virtual environment..."
|
||||||
|
python3 -m venv venv
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Activate virtual environment
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
echo "Installing Python dependencies..."
|
||||||
|
pip install -q -r requirements.txt
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
if python run_all_tests.py; then
|
||||||
|
report_result "Integration Tests" 0
|
||||||
|
else
|
||||||
|
report_result "Integration Tests" 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
deactivate
|
||||||
|
cd ../..
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠️ Integration tests directory not found, skipping...${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Final Summary
|
||||||
|
echo ""
|
||||||
|
echo "=================================="
|
||||||
|
echo "🏁 Test Suite Complete!"
|
||||||
|
echo "=================================="
|
||||||
|
echo -e "${GREEN}Passed: $TESTS_PASSED${NC}"
|
||||||
|
echo -e "${RED}Failed: $TESTS_FAILED${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$TESTS_FAILED" -gt 0 ]; then
|
||||||
|
echo -e "${RED}❌ Some tests failed. Please review the output above.${NC}"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✅ All tests passed successfully!${NC}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
10
.github/workflows/scripts/schemasync/go.mod
vendored
Normal file
10
.github/workflows/scripts/schemasync/go.mod
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
module github.com/maximhq/bifrost/tools/schema-sync
|
||||||
|
|
||||||
|
go 1.26.2
|
||||||
|
|
||||||
|
require golang.org/x/tools v0.30.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
golang.org/x/mod v0.23.0 // indirect
|
||||||
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
|
)
|
||||||
8
.github/workflows/scripts/schemasync/go.sum
vendored
Normal file
8
.github/workflows/scripts/schemasync/go.sum
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||||
|
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||||
|
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||||
1075
.github/workflows/scripts/schemasync/main.go
vendored
Normal file
1075
.github/workflows/scripts/schemasync/main.go
vendored
Normal file
File diff suppressed because it is too large
Load Diff
35
.github/workflows/scripts/setup-go-workspace.sh
vendored
Executable file
35
.github/workflows/scripts/setup-go-workspace.sh
vendored
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
export GOTOOLCHAIN=auto
|
||||||
|
|
||||||
|
# If go.work exists, skip
|
||||||
|
if [ -f "go.work" ]; then
|
||||||
|
echo "🔍 Go workspace already exists, skipping initialization"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Setup Go workspace for CI
|
||||||
|
# Usage: source setup-go-workspace.sh
|
||||||
|
echo "🔧 Setting up Go workspace..."
|
||||||
|
if [ -f "go.work" ]; then
|
||||||
|
echo "✅ Go workspace already exists, skipping init"
|
||||||
|
return 0 2>/dev/null || exit 0
|
||||||
|
fi
|
||||||
|
go work init
|
||||||
|
go work use ./core
|
||||||
|
go work use ./framework
|
||||||
|
go work use ./plugins/compat
|
||||||
|
go work use ./plugins/governance
|
||||||
|
go work use ./plugins/jsonparser
|
||||||
|
go work use ./plugins/logging
|
||||||
|
go work use ./plugins/maxim
|
||||||
|
go work use ./plugins/mocker
|
||||||
|
go work use ./plugins/otel
|
||||||
|
go work use ./plugins/prompts
|
||||||
|
go work use ./plugins/semanticcache
|
||||||
|
go work use ./plugins/telemetry
|
||||||
|
go work use ./transports
|
||||||
|
go work use ./cli
|
||||||
|
echo "✅ Go workspace initialized"
|
||||||
179
.github/workflows/scripts/test-all-plugins.sh
vendored
Executable file
179
.github/workflows/scripts/test-all-plugins.sh
vendored
Executable file
@@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Test all plugins
|
||||||
|
# Usage: ./test-all-plugins.sh [<JSON_ARRAY_OF_PLUGINS>]
|
||||||
|
# If no argument provided, tests all plugins in the plugins/ directory
|
||||||
|
|
||||||
|
# Setup Go workspace for CI
|
||||||
|
source "$(dirname "$0")/setup-go-workspace.sh"
|
||||||
|
|
||||||
|
echo "🧪 Running plugin tests..."
|
||||||
|
|
||||||
|
# Cleanup function to ensure Docker services are stopped
|
||||||
|
cleanup_docker() {
|
||||||
|
echo "🧹 Cleaning up Docker services..."
|
||||||
|
if command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
docker-compose -f tests/docker-compose.yml down 2>/dev/null || true
|
||||||
|
elif docker compose version >/dev/null 2>&1; then
|
||||||
|
docker compose -f tests/docker-compose.yml down 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Register cleanup handler to run on script exit (success or failure)
|
||||||
|
trap cleanup_docker EXIT
|
||||||
|
|
||||||
|
# Starting dependencies of plugin tests
|
||||||
|
echo "🔧 Starting dependencies of plugin tests..."
|
||||||
|
# Use docker compose (v2) if available, fallback to docker-compose (v1)
|
||||||
|
if command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
docker-compose -f tests/docker-compose.yml up -d
|
||||||
|
elif docker compose version >/dev/null 2>&1; then
|
||||||
|
docker compose -f tests/docker-compose.yml up -d
|
||||||
|
else
|
||||||
|
echo "❌ Neither docker-compose nor docker compose is available"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 20
|
||||||
|
|
||||||
|
# Determine which plugins to test
|
||||||
|
if [ $# -gt 0 ] && [ -n "$1" ]; then
|
||||||
|
CHANGED_PLUGINS_JSON="$1"
|
||||||
|
|
||||||
|
# Verify jq is available
|
||||||
|
if ! command -v jq >/dev/null 2>&1; then
|
||||||
|
echo "❌ Error: jq is required but not installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate that the input is valid JSON
|
||||||
|
if ! echo "$CHANGED_PLUGINS_JSON" | jq empty >/dev/null 2>&1; then
|
||||||
|
echo "❌ Error: Invalid JSON provided"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# No work early‐exit if array is empty
|
||||||
|
if jq -e 'length==0' <<<"$CHANGED_PLUGINS_JSON" >/dev/null 2>&1; then
|
||||||
|
echo "⏭️ No plugins to test"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Convert JSON array to bash array
|
||||||
|
if ! readarray -t PLUGINS < <(echo "$CHANGED_PLUGINS_JSON" | jq -r '.[]' 2>/dev/null); then
|
||||||
|
echo "❌ Error: Failed to parse plugin names from JSON"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Test all plugins in the plugins/ directory
|
||||||
|
PLUGINS=()
|
||||||
|
for plugin_dir in plugins/*/; do
|
||||||
|
if [ -d "$plugin_dir" ] && [ -f "$plugin_dir/go.mod" ]; then
|
||||||
|
plugin_name=$(basename "$plugin_dir")
|
||||||
|
PLUGINS+=("$plugin_name")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#PLUGINS[@]} -eq 0 ]; then
|
||||||
|
echo "⏭️ No plugins to test"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔌 Testing ${#PLUGINS[@]} plugins:"
|
||||||
|
for p in "${PLUGINS[@]}"; do
|
||||||
|
echo " • $p"
|
||||||
|
done
|
||||||
|
|
||||||
|
FAILED_PLUGINS=()
|
||||||
|
SUCCESS_COUNT=0
|
||||||
|
OVERALL_EXIT_CODE=0
|
||||||
|
|
||||||
|
# Test each plugin
|
||||||
|
for plugin in "${PLUGINS[@]}"; do
|
||||||
|
echo ""
|
||||||
|
echo "🔌 Testing plugin: $plugin"
|
||||||
|
|
||||||
|
PLUGIN_DIR="plugins/$plugin"
|
||||||
|
|
||||||
|
if [ ! -d "$PLUGIN_DIR" ]; then
|
||||||
|
echo "⚠️ Warning: Plugin directory not found: $PLUGIN_DIR (skipping)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$PLUGIN_DIR/go.mod" ]; then
|
||||||
|
echo "ℹ️ No go.mod found for $plugin, skipping tests"
|
||||||
|
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$PLUGIN_DIR"
|
||||||
|
|
||||||
|
# Validate build
|
||||||
|
echo "🔨 Validating plugin build..."
|
||||||
|
if ! go build ./...; then
|
||||||
|
echo "❌ Build failed for plugin: $plugin"
|
||||||
|
FAILED_PLUGINS+=("$plugin")
|
||||||
|
OVERALL_EXIT_CODE=1
|
||||||
|
cd ../..
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run tests with coverage if any exist
|
||||||
|
if go list ./... | grep -q .; then
|
||||||
|
# Run E2E tests for governance plugin (currently disabled)
|
||||||
|
if [ "$plugin" = "governance" ]; then
|
||||||
|
echo "🧪 Running governance plugin tests..."
|
||||||
|
# Governance plugin tests are currently disabled in release script
|
||||||
|
# Just run regular tests
|
||||||
|
if go test -v -timeout 20m -coverprofile=coverage.txt -coverpkg=./... ./...; then
|
||||||
|
echo "✅ Tests passed for: $plugin"
|
||||||
|
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||||
|
else
|
||||||
|
echo "❌ Tests failed for: $plugin"
|
||||||
|
FAILED_PLUGINS+=("$plugin")
|
||||||
|
OVERALL_EXIT_CODE=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "🧪 Running plugin tests with coverage..."
|
||||||
|
if go test -v -timeout 20m -coverprofile=coverage.txt -coverpkg=./... ./...; then
|
||||||
|
echo "✅ Tests passed for: $plugin"
|
||||||
|
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||||
|
else
|
||||||
|
echo "❌ Tests failed for: $plugin"
|
||||||
|
FAILED_PLUGINS+=("$plugin")
|
||||||
|
OVERALL_EXIT_CODE=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Upload coverage to Codecov
|
||||||
|
if [ -n "${CODECOV_TOKEN:-}" ] && [ -f coverage.txt ]; then
|
||||||
|
echo "📊 Uploading coverage to Codecov..."
|
||||||
|
curl -Os https://uploader.codecov.io/latest/linux/codecov
|
||||||
|
chmod +x codecov
|
||||||
|
./codecov -t "$CODECOV_TOKEN" -f coverage.txt -F "plugin-${plugin}"
|
||||||
|
rm -f codecov coverage.txt
|
||||||
|
else
|
||||||
|
rm -f coverage.txt
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "ℹ️ No tests found for $plugin"
|
||||||
|
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd ../..
|
||||||
|
done
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo ""
|
||||||
|
echo "📋 Plugin Test Summary:"
|
||||||
|
echo " ✅ Successful: $SUCCESS_COUNT/${#PLUGINS[@]}"
|
||||||
|
echo " ❌ Failed: ${#FAILED_PLUGINS[@]}"
|
||||||
|
|
||||||
|
if [ ${#FAILED_PLUGINS[@]} -gt 0 ]; then
|
||||||
|
echo " Failed plugins: ${FAILED_PLUGINS[*]}"
|
||||||
|
echo "❌ Plugin tests completed with failures"
|
||||||
|
exit $OVERALL_EXIT_CODE
|
||||||
|
else
|
||||||
|
echo " 🎉 All plugin tests passed!"
|
||||||
|
echo "✅ Plugin tests completed successfully"
|
||||||
|
fi
|
||||||
236
.github/workflows/scripts/test-bifrost-http.sh
vendored
Executable file
236
.github/workflows/scripts/test-bifrost-http.sh
vendored
Executable file
@@ -0,0 +1,236 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Test bifrost-http component
|
||||||
|
# Usage: ./test-bifrost-http.sh
|
||||||
|
|
||||||
|
# Get the absolute path of the script directory
|
||||||
|
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||||
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
|
else
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Setup Go workspace for CI
|
||||||
|
source "$(dirname "$0")/setup-go-workspace.sh"
|
||||||
|
|
||||||
|
echo "🧪 Running bifrost-http tests..."
|
||||||
|
|
||||||
|
# Validate that config.schema.json and values.schema.json are in sync
|
||||||
|
echo "🔍 Validating schema consistency between config.schema.json and values.schema.json..."
|
||||||
|
VALIDATE_SCHEMA_SCRIPT="$SCRIPT_DIR/validate-helm-schema.sh"
|
||||||
|
if [ -f "$VALIDATE_SCHEMA_SCRIPT" ]; then
|
||||||
|
if ! "$VALIDATE_SCHEMA_SCRIPT"; then
|
||||||
|
echo "❌ Schema validation failed. The Helm chart values.schema.json is not in sync with config.schema.json"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Schema validation passed"
|
||||||
|
else
|
||||||
|
echo "⚠️ Warning: validate-helm-schema.sh not found, skipping schema validation"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cleanup function to ensure Docker services are stopped
|
||||||
|
cleanup_docker() {
|
||||||
|
echo "🧹 Cleaning up Docker services..."
|
||||||
|
docker compose -f "$CONFIGS_DIR/docker-compose.yml" down 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
CONFIGS_DIR=".github/workflows/configs"
|
||||||
|
|
||||||
|
# Register cleanup handler to run on script exit (success or failure)
|
||||||
|
trap cleanup_docker EXIT
|
||||||
|
|
||||||
|
# Build UI first before we can validate the transport build
|
||||||
|
echo "🎨 Building UI..."
|
||||||
|
make build-ui
|
||||||
|
|
||||||
|
# Building hello-world plugin
|
||||||
|
echo "🔨 Building hello-world plugin..."
|
||||||
|
cd examples/plugins/hello-world
|
||||||
|
make build
|
||||||
|
cd ../../..
|
||||||
|
|
||||||
|
# Validate transport build
|
||||||
|
echo "🔨 Validating transport build..."
|
||||||
|
cd transports
|
||||||
|
go build ./...
|
||||||
|
|
||||||
|
# Run unit tests with coverage
|
||||||
|
echo "🧪 Running unit tests with coverage..."
|
||||||
|
go test --race -v -timeout 40m -coverprofile=coverage.txt ./...
|
||||||
|
|
||||||
|
# Upload coverage to Codecov
|
||||||
|
if [ -n "${CODECOV_TOKEN:-}" ]; then
|
||||||
|
echo "📊 Uploading coverage to Codecov..."
|
||||||
|
curl -Os https://uploader.codecov.io/latest/linux/codecov
|
||||||
|
chmod +x codecov
|
||||||
|
./codecov -t "$CODECOV_TOKEN" -f coverage.txt -F transports
|
||||||
|
rm -f codecov coverage.txt
|
||||||
|
else
|
||||||
|
echo "ℹ️ CODECOV_TOKEN not set, skipping coverage upload"
|
||||||
|
rm -f coverage.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build the binary for integration testing
|
||||||
|
echo "🔨 Building binary for integration testing..."
|
||||||
|
mkdir -p ../tmp
|
||||||
|
cd bifrost-http
|
||||||
|
go build -o ../../tmp/bifrost-http .
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Run integration tests with different configurations
|
||||||
|
echo "🧪 Running integration tests with different configurations..."
|
||||||
|
CONFIGS_TO_TEST=(
|
||||||
|
"default"
|
||||||
|
"emptystate"
|
||||||
|
"noconfigstorenologstore"
|
||||||
|
"witconfigstorelogstorepostgres"
|
||||||
|
"withconfigstore"
|
||||||
|
"withconfigstorelogsstorepostgres"
|
||||||
|
"withconfigstorelogsstoresqlite"
|
||||||
|
"withdynamicplugin"
|
||||||
|
"withobservability"
|
||||||
|
"withsemanticcache"
|
||||||
|
"withpostgresmcpclientsinconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
TEST_BINARY="../tmp/bifrost-http"
|
||||||
|
CONFIGS_DIR="../.github/workflows/configs"
|
||||||
|
|
||||||
|
# Running docker compose
|
||||||
|
echo "🐳 Starting Docker services (PostgreSQL, Weaviate, Redis)..."
|
||||||
|
docker compose -f "$CONFIGS_DIR/docker-compose.yml" up -d
|
||||||
|
|
||||||
|
# Wait for services to be healthy with polling
|
||||||
|
echo "⏳ Waiting for Docker services to be ready..."
|
||||||
|
MAX_WAIT=300
|
||||||
|
ELAPSED=0
|
||||||
|
SERVICES_READY=false
|
||||||
|
|
||||||
|
# Get expected number of services
|
||||||
|
EXPECTED_SERVICES=$(docker compose -f "$CONFIGS_DIR/docker-compose.yml" config --services 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
|
||||||
|
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||||
|
# Get running container count
|
||||||
|
RUNNING_COUNT=$(docker compose -f "$CONFIGS_DIR/docker-compose.yml" ps --status running -q 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
|
||||||
|
# Check health status: count healthy and unhealthy (starting/unhealthy) services
|
||||||
|
HEALTH_OUTPUT=$(docker compose -f "$CONFIGS_DIR/docker-compose.yml" ps --format "{{.Name}}:{{.Health}}" 2>/dev/null)
|
||||||
|
HEALTHY_COUNT=$(echo "$HEALTH_OUTPUT" | grep -c ":healthy") || HEALTHY_COUNT=0
|
||||||
|
UNHEALTHY_COUNT=$(echo "$HEALTH_OUTPUT" | grep -cE ":(starting|unhealthy)") || UNHEALTHY_COUNT=0
|
||||||
|
|
||||||
|
# All services are ready when:
|
||||||
|
# 1. All expected services are running
|
||||||
|
# 2. No services are in "starting" or "unhealthy" state
|
||||||
|
if [ "$RUNNING_COUNT" -eq "$EXPECTED_SERVICES" ] && [ "$UNHEALTHY_COUNT" -eq "0" ]; then
|
||||||
|
SERVICES_READY=true
|
||||||
|
echo "✅ All Docker services are ready ($HEALTHY_COUNT with healthchecks, ${ELAPSED}s)"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
ELAPSED=$((ELAPSED + 2))
|
||||||
|
echo " ⏳ Waiting for services... ($RUNNING_COUNT/$EXPECTED_SERVICES running, $HEALTHY_COUNT healthy, $UNHEALTHY_COUNT starting, ${ELAPSED}s/${MAX_WAIT}s)"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$SERVICES_READY" = false ]; then
|
||||||
|
echo "❌ Docker services failed to become healthy within ${MAX_WAIT}s"
|
||||||
|
echo " Current service status:"
|
||||||
|
docker compose -f "$CONFIGS_DIR/docker-compose.yml" ps
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for config in "${CONFIGS_TO_TEST[@]}"; do
|
||||||
|
echo " 🔍 Testing with config: $config"
|
||||||
|
config_path="$CONFIGS_DIR/$config"
|
||||||
|
|
||||||
|
# Clean up databases before each config test for a clean slate
|
||||||
|
echo " 🧹 Resetting PostgreSQL database..."
|
||||||
|
docker exec -e PGPASSWORD=bifrost_password "$(docker compose -f "$CONFIGS_DIR/docker-compose.yml" ps -q postgres)" \
|
||||||
|
psql -U bifrost -d postgres \
|
||||||
|
-c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'bifrost' AND pid <> pg_backend_pid();" \
|
||||||
|
-c "DROP DATABASE IF EXISTS bifrost;" \
|
||||||
|
-c "CREATE DATABASE bifrost;"
|
||||||
|
|
||||||
|
echo " 🧹 Cleaning up SQLite database files for config: $config..."
|
||||||
|
find "$config_path" -type f \( -name "*.db" -o -name "*.db-shm" -o -name "*.db-wal" \) -delete 2>/dev/null || true
|
||||||
|
echo " ✅ Database cleanup complete"
|
||||||
|
|
||||||
|
if [ ! -d "$config_path" ]; then
|
||||||
|
echo " ⚠️ Warning: Config directory not found: $config_path (skipping)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create a temporary log file for server output
|
||||||
|
SERVER_LOG=$(mktemp)
|
||||||
|
|
||||||
|
# Start the server in background with a timeout, logging to file and console
|
||||||
|
timeout 120s $TEST_BINARY --app-dir "$config_path" --port 18080 --log-level debug 2>&1 | tee "$SERVER_LOG" &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
# Wait for server to be ready by looking for the startup message
|
||||||
|
echo " ⏳ Waiting for server to start..."
|
||||||
|
MAX_WAIT=30
|
||||||
|
ELAPSED=0
|
||||||
|
SERVER_READY=false
|
||||||
|
|
||||||
|
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||||
|
if grep -q "successfully started bifrost, serving UI on http://localhost:18080" "$SERVER_LOG" 2>/dev/null; then
|
||||||
|
SERVER_READY=true
|
||||||
|
echo " ✅ Server started successfully with config: $config"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if server process is still running
|
||||||
|
if ! kill -0 $SERVER_PID 2>/dev/null; then
|
||||||
|
echo " ❌ Server process died before starting with config: $config"
|
||||||
|
rm -f "$SERVER_LOG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
ELAPSED=$((ELAPSED + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$SERVER_READY" = false ]; then
|
||||||
|
echo " ❌ Server failed to start within ${MAX_WAIT}s with config: $config"
|
||||||
|
kill $SERVER_PID 2>/dev/null || true
|
||||||
|
wait $SERVER_PID 2>/dev/null || true
|
||||||
|
rm -f "$SERVER_LOG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run get_curls.sh to test all GET endpoints
|
||||||
|
echo " 🧪 Running API endpoint tests..."
|
||||||
|
GET_CURLS_SCRIPT="$SCRIPT_DIR/get_curls.sh"
|
||||||
|
|
||||||
|
if [ -f "$GET_CURLS_SCRIPT" ]; then
|
||||||
|
BASE_URL="http://localhost:18080" "$GET_CURLS_SCRIPT"
|
||||||
|
CURL_EXIT_CODE=$?
|
||||||
|
|
||||||
|
if [ $CURL_EXIT_CODE -eq 0 ]; then
|
||||||
|
echo " ✅ API endpoint tests passed for config: $config"
|
||||||
|
else
|
||||||
|
echo " ❌ API endpoint tests failed for config: $config (exit code: $CURL_EXIT_CODE)"
|
||||||
|
kill $SERVER_PID 2>/dev/null || true
|
||||||
|
wait $SERVER_PID 2>/dev/null || true
|
||||||
|
rm -f "$SERVER_LOG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ⚠️ Warning: get_curls.sh not found at $GET_CURLS_SCRIPT (skipping endpoint tests)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Kill the server
|
||||||
|
kill $SERVER_PID 2>/dev/null || true
|
||||||
|
wait $SERVER_PID 2>/dev/null || true
|
||||||
|
|
||||||
|
# Clean up log file
|
||||||
|
rm -f "$SERVER_LOG"
|
||||||
|
|
||||||
|
# Clean up any lingering processes
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
echo "✅ Bifrost-HTTP tests completed successfully"
|
||||||
57
.github/workflows/scripts/test-core.sh
vendored
Executable file
57
.github/workflows/scripts/test-core.sh
vendored
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Test core component
|
||||||
|
# Usage: ./test-core.sh
|
||||||
|
|
||||||
|
# Setup Go workspace for CI
|
||||||
|
source "$(dirname "$0")/setup-go-workspace.sh"
|
||||||
|
|
||||||
|
echo "🧪 Running core tests..."
|
||||||
|
|
||||||
|
# Build MCP test servers for STDIO tests
|
||||||
|
echo "🔧 Building MCP test servers..."
|
||||||
|
for mcp_dir in examples/mcps/*/; do
|
||||||
|
if [ -d "$mcp_dir" ]; then
|
||||||
|
mcp_name=$(basename "$mcp_dir")
|
||||||
|
if [ -f "$mcp_dir/go.mod" ]; then
|
||||||
|
echo " Building $mcp_name (Go)..."
|
||||||
|
mkdir -p "$mcp_dir/bin"
|
||||||
|
pushd "$mcp_dir" > /dev/null
|
||||||
|
GOWORK=off go build -o "bin/$mcp_name" .
|
||||||
|
popd > /dev/null
|
||||||
|
elif [ -f "$mcp_dir/package.json" ]; then
|
||||||
|
echo " Building $mcp_name (TypeScript)..."
|
||||||
|
pushd "$mcp_dir" > /dev/null
|
||||||
|
npm install --silent && npm run build
|
||||||
|
popd > /dev/null
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "✅ MCP test servers built"
|
||||||
|
|
||||||
|
# Validate core build
|
||||||
|
echo "🔨 Validating core build..."
|
||||||
|
cd core
|
||||||
|
go mod download
|
||||||
|
go build ./...
|
||||||
|
echo "✅ Core build validation successful"
|
||||||
|
|
||||||
|
# Run core tests with coverage
|
||||||
|
echo "🧪 Running core tests with coverage..."
|
||||||
|
go test -race -timeout 20m -coverprofile=coverage.txt -coverpkg=./... ./...
|
||||||
|
|
||||||
|
# Upload coverage to Codecov
|
||||||
|
if [ -n "${CODECOV_TOKEN:-}" ]; then
|
||||||
|
echo "📊 Uploading coverage to Codecov..."
|
||||||
|
curl -Os https://uploader.codecov.io/latest/linux/codecov
|
||||||
|
chmod +x codecov
|
||||||
|
./codecov -t "$CODECOV_TOKEN" -f coverage.txt -F core
|
||||||
|
rm -f codecov coverage.txt
|
||||||
|
else
|
||||||
|
echo "ℹ️ CODECOV_TOKEN not set, skipping coverage upload"
|
||||||
|
rm -f coverage.txt
|
||||||
|
fi
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
echo "✅ Core tests completed successfully"
|
||||||
301
.github/workflows/scripts/test-docker-image.sh
vendored
Executable file
301
.github/workflows/scripts/test-docker-image.sh
vendored
Executable file
@@ -0,0 +1,301 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Test Docker image by building, starting with docker-compose, and running E2E API tests
|
||||||
|
# Usage: ./test-docker-image.sh <platform>
|
||||||
|
# Example: ./test-docker-image.sh linux/amd64
|
||||||
|
|
||||||
|
# Get the absolute path of the script directory
|
||||||
|
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||||
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
|
else
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Repository root (3 levels up from .github/workflows/scripts)
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd -P)"
|
||||||
|
|
||||||
|
|
||||||
|
# Setup Go workspace for CI (go.work is gitignored, must be regenerated)
|
||||||
|
source "$SCRIPT_DIR/setup-go-workspace.sh"
|
||||||
|
|
||||||
|
PLATFORM=${1:-linux/amd64}
|
||||||
|
ARCH=$(echo "$PLATFORM" | cut -d'/' -f2)
|
||||||
|
IMAGE_TAG="bifrost-test:ci-${GITHUB_SHA:-local}-${ARCH}"
|
||||||
|
CONTAINER_NAME="bifrost-test-${ARCH}"
|
||||||
|
TEST_PORT=8080
|
||||||
|
DOCKER_COMPOSE_FILE="$REPO_ROOT/tests/docker-compose.yml"
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
CONFIG_FILE="$TEMP_DIR/config.json"
|
||||||
|
|
||||||
|
echo "=== Testing Docker image for ${PLATFORM} ==="
|
||||||
|
|
||||||
|
# Cleanup function
|
||||||
|
cleanup() {
|
||||||
|
local exit_code=$?
|
||||||
|
echo ""
|
||||||
|
echo "=== Cleaning up ==="
|
||||||
|
|
||||||
|
# Stop and remove Bifrost container
|
||||||
|
echo "Stopping Bifrost container..."
|
||||||
|
docker stop "${CONTAINER_NAME}" > /dev/null 2>&1 || true
|
||||||
|
docker rm "${CONTAINER_NAME}" > /dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Stop docker-compose services
|
||||||
|
echo "Stopping docker-compose services..."
|
||||||
|
docker compose -f "$DOCKER_COMPOSE_FILE" down -v > /dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Remove test image
|
||||||
|
echo "Removing test image..."
|
||||||
|
docker rmi "${IMAGE_TAG}" > /dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Remove temp directory
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
|
||||||
|
exit $exit_code
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Build the image using local module sources (pre-release CI builds)
|
||||||
|
echo "Building Docker image (local modules)..."
|
||||||
|
docker build \
|
||||||
|
--platform "${PLATFORM}" \
|
||||||
|
-f transports/Dockerfile.local \
|
||||||
|
-t "${IMAGE_TAG}" \
|
||||||
|
.
|
||||||
|
|
||||||
|
echo "Build complete: ${IMAGE_TAG}"
|
||||||
|
|
||||||
|
# Start docker-compose services (Postgres, Weaviate, Redis, Qdrant)
|
||||||
|
echo ""
|
||||||
|
echo "=== Starting docker-compose services ==="
|
||||||
|
docker compose -f "$DOCKER_COMPOSE_FILE" up -d
|
||||||
|
|
||||||
|
# Wait for Postgres to be ready
|
||||||
|
echo "Waiting for Postgres to be ready..."
|
||||||
|
MAX_WAIT=60
|
||||||
|
ELAPSED=0
|
||||||
|
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||||
|
if docker compose -f "$DOCKER_COMPOSE_FILE" exec -T postgres pg_isready -U bifrost -d bifrost > /dev/null 2>&1; then
|
||||||
|
echo "Postgres is ready"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
ELAPSED=$((ELAPSED + 2))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $ELAPSED -ge $MAX_WAIT ]; then
|
||||||
|
echo "ERROR: Postgres did not become ready within ${MAX_WAIT}s"
|
||||||
|
docker compose -f "$DOCKER_COMPOSE_FILE" logs postgres
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get the docker network name
|
||||||
|
NETWORK_NAME=$(docker compose -f "$DOCKER_COMPOSE_FILE" ps --format json | head -1 | jq -r '.Networks' 2>/dev/null || echo "tests_bifrost_network")
|
||||||
|
if [ -z "$NETWORK_NAME" ] || [ "$NETWORK_NAME" = "null" ]; then
|
||||||
|
NETWORK_NAME="tests_bifrost_network"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate config.json with all providers and Postgres stores
|
||||||
|
echo ""
|
||||||
|
echo "=== Generating config.json ==="
|
||||||
|
cat > "$CONFIG_FILE" << 'CONFIGEOF'
|
||||||
|
{
|
||||||
|
"$schema": "https://www.getbifrost.ai/schema",
|
||||||
|
"providers": {
|
||||||
|
"openai": {
|
||||||
|
"keys": [{ "name": "OpenAI API Key", "value": "env.OPENAI_API_KEY", "weight": 1, "use_for_batch_api": true }],
|
||||||
|
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||||
|
},
|
||||||
|
"elevenlabs": {
|
||||||
|
"keys": [{ "name": "ElevenLabs API Key", "value": "env.ELEVENLABS_API_KEY", "weight": 1, "use_for_batch_api": true }],
|
||||||
|
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||||
|
},
|
||||||
|
"xai": {
|
||||||
|
"keys": [{ "name": "Xai API Key", "value": "env.XAI_API_KEY", "weight": 1, "use_for_batch_api": true }],
|
||||||
|
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||||
|
},
|
||||||
|
"huggingface": {
|
||||||
|
"keys": [{ "name": "Hugging Face API Key", "value": "env.HUGGING_FACE_API_KEY", "weight": 1, "use_for_batch_api": true }],
|
||||||
|
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||||
|
},
|
||||||
|
"anthropic": {
|
||||||
|
"keys": [{ "name": "Anthropic API Key", "value": "env.ANTHROPIC_API_KEY", "weight": 1, "use_for_batch_api": true }],
|
||||||
|
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||||
|
},
|
||||||
|
"gemini": {
|
||||||
|
"keys": [{ "value": "env.GEMINI_API_KEY", "weight": 1, "use_for_batch_api": true, "name": "Gemini API Key" }],
|
||||||
|
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||||
|
},
|
||||||
|
"vertex": {
|
||||||
|
"keys": [{ "name": "Vertex API Key", "vertex_key_config": { "project_id": "env.VERTEX_PROJECT_ID", "region": "env.GOOGLE_LOCATION", "auth_credentials": "env.VERTEX_CREDENTIALS" }, "weight": 1 }],
|
||||||
|
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||||
|
},
|
||||||
|
"mistral": {
|
||||||
|
"keys": [{ "name": "Mistral API Key", "value": "env.MISTRAL_API_KEY", "weight": 1 }],
|
||||||
|
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||||
|
},
|
||||||
|
"cohere": {
|
||||||
|
"keys": [{ "name": "Cohere API Key", "value": "env.COHERE_API_KEY", "weight": 1 }],
|
||||||
|
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||||
|
},
|
||||||
|
"groq": {
|
||||||
|
"keys": [{ "name": "Groq API Key", "value": "env.GROQ_API_KEY", "weight": 1 }],
|
||||||
|
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||||
|
},
|
||||||
|
"perplexity": {
|
||||||
|
"keys": [{ "name": "Perplexity API Key", "value": "env.PERPLEXITY_API_KEY", "weight": 1 }],
|
||||||
|
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||||
|
},
|
||||||
|
"cerebras": {
|
||||||
|
"keys": [{ "name": "Cerebras API Key", "value": "env.CEREBRAS_API_KEY", "weight": 1 }],
|
||||||
|
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||||
|
},
|
||||||
|
"openrouter": {
|
||||||
|
"keys": [{ "name": "OpenRouter API Key", "value": "env.OPENROUTER_API_KEY", "weight": 1 }],
|
||||||
|
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||||
|
},
|
||||||
|
"parasail": {
|
||||||
|
"keys": [{ "name": "Parasail API Key", "value": "env.PARASAIL_API_KEY", "weight": 1 }],
|
||||||
|
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||||
|
},
|
||||||
|
"azure": {
|
||||||
|
"keys": [{ "name": "Azure API Key", "value": "env.AZURE_API_KEY", "azure_key_config": { "endpoint": "env.AZURE_ENDPOINT", "api_version": "env.AZURE_API_VERSION" }, "weight": 1 }],
|
||||||
|
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||||
|
},
|
||||||
|
"bedrock": {
|
||||||
|
"keys": [{ "name": "Bedrock API Key", "bedrock_key_config": { "access_key": "env.AWS_ACCESS_KEY_ID", "secret_key": "env.AWS_SECRET_ACCESS_KEY", "region": "env.AWS_REGION", "arn": "env.AWS_ARN" }, "weight": 1, "use_for_batch_api": true }],
|
||||||
|
"network_config": { "default_request_timeout_in_seconds": 300 }
|
||||||
|
},
|
||||||
|
"replicate": {
|
||||||
|
"keys": [{ "name": "Replicate API KEY", "value": "env.REPLICATE_API_KEY", "weight": 1.0, "use_for_batch_api": true }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "postgres",
|
||||||
|
"config": {
|
||||||
|
"host": "postgres",
|
||||||
|
"port": "5432",
|
||||||
|
"user": "bifrost",
|
||||||
|
"password": "bifrost_password",
|
||||||
|
"db_name": "bifrost",
|
||||||
|
"ssl_mode": "disable"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logs_store": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "postgres",
|
||||||
|
"config": {
|
||||||
|
"host": "postgres",
|
||||||
|
"port": "5432",
|
||||||
|
"user": "bifrost",
|
||||||
|
"password": "bifrost_password",
|
||||||
|
"db_name": "bifrost",
|
||||||
|
"ssl_mode": "disable"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"governance": {
|
||||||
|
"virtual_keys": [
|
||||||
|
{
|
||||||
|
"id": "vk-test",
|
||||||
|
"value": "sk-bf-test-key",
|
||||||
|
"is_active": true,
|
||||||
|
"name": "vk-test"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"drop_excess_requests": false,
|
||||||
|
"initial_pool_size": 300,
|
||||||
|
"allowed_origins": ["http://localhost:3000", "https://localhost:3000"],
|
||||||
|
"enable_logging": true,
|
||||||
|
"enforce_governance_header": false,
|
||||||
|
"allow_direct_keys": false,
|
||||||
|
"max_request_body_size_mb": 100
|
||||||
|
},
|
||||||
|
"encryption_key": ""
|
||||||
|
}
|
||||||
|
CONFIGEOF
|
||||||
|
|
||||||
|
echo "Config file created at: $CONFIG_FILE"
|
||||||
|
|
||||||
|
# Run the Bifrost container connected to the docker-compose network
|
||||||
|
echo ""
|
||||||
|
echo "=== Starting Bifrost container ==="
|
||||||
|
docker run -d \
|
||||||
|
--name "${CONTAINER_NAME}" \
|
||||||
|
--platform "${PLATFORM}" \
|
||||||
|
--network "${NETWORK_NAME}" \
|
||||||
|
-p ${TEST_PORT}:8080 \
|
||||||
|
-e APP_PORT=8080 \
|
||||||
|
-e APP_HOST=0.0.0.0 \
|
||||||
|
-e OPENAI_API_KEY="${OPENAI_API_KEY:-}" \
|
||||||
|
-e ELEVENLABS_API_KEY="${ELEVENLABS_API_KEY:-}" \
|
||||||
|
-e XAI_API_KEY="${XAI_API_KEY:-}" \
|
||||||
|
-e HUGGING_FACE_API_KEY="${HUGGING_FACE_API_KEY:-}" \
|
||||||
|
-e ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-}" \
|
||||||
|
-e GEMINI_API_KEY="${GEMINI_API_KEY:-}" \
|
||||||
|
-e VERTEX_PROJECT_ID="${VERTEX_PROJECT_ID:-}" \
|
||||||
|
-e VERTEX_CREDENTIALS="${VERTEX_CREDENTIALS:-}" \
|
||||||
|
-e GOOGLE_LOCATION="${GOOGLE_LOCATION:-us-central1}" \
|
||||||
|
-e MISTRAL_API_KEY="${MISTRAL_API_KEY:-}" \
|
||||||
|
-e COHERE_API_KEY="${COHERE_API_KEY:-}" \
|
||||||
|
-e GROQ_API_KEY="${GROQ_API_KEY:-}" \
|
||||||
|
-e PERPLEXITY_API_KEY="${PERPLEXITY_API_KEY:-}" \
|
||||||
|
-e CEREBRAS_API_KEY="${CEREBRAS_API_KEY:-}" \
|
||||||
|
-e OPENROUTER_API_KEY="${OPENROUTER_API_KEY:-}" \
|
||||||
|
-e PARASAIL_API_KEY="${PARASAIL_API_KEY:-}" \
|
||||||
|
-e AZURE_API_KEY="${AZURE_API_KEY:-}" \
|
||||||
|
-e AZURE_ENDPOINT="${AZURE_ENDPOINT:-}" \
|
||||||
|
-e AZURE_API_VERSION="${AZURE_API_VERSION:-}" \
|
||||||
|
-e AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}" \
|
||||||
|
-e AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}" \
|
||||||
|
-e AWS_REGION="${AWS_REGION:-us-east-1}" \
|
||||||
|
-e AWS_ARN="${AWS_ARN:-}" \
|
||||||
|
-e REPLICATE_API_KEY="${REPLICATE_API_KEY:-}" \
|
||||||
|
-v "$CONFIG_FILE:/app/data/config.json:ro" \
|
||||||
|
"${IMAGE_TAG}"
|
||||||
|
|
||||||
|
# Wait for Bifrost to be ready
|
||||||
|
echo "Waiting for Bifrost to start..."
|
||||||
|
MAX_WAIT=60
|
||||||
|
ELAPSED=0
|
||||||
|
HEALTH_OK=0
|
||||||
|
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||||
|
if curl -sf "http://localhost:${TEST_PORT}/health" > /dev/null 2>&1; then
|
||||||
|
echo "Bifrost health check passed (attempt $((ELAPSED/2 + 1)))"
|
||||||
|
HEALTH_OK=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
ELAPSED=$((ELAPSED + 2))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $HEALTH_OK -eq 0 ]; then
|
||||||
|
echo "ERROR: Bifrost health check failed!"
|
||||||
|
echo "Container logs:"
|
||||||
|
docker logs "${CONTAINER_NAME}" 2>&1 | tail -100 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# # Run E2E API tests
|
||||||
|
# echo ""
|
||||||
|
# echo "=== Running E2E API tests ==="
|
||||||
|
# export BIFROST_BASE_URL="http://localhost:${TEST_PORT}"
|
||||||
|
# export CI=1
|
||||||
|
|
||||||
|
# echo pwd: $(pwd)
|
||||||
|
# # Run the E2E API test scripts (marked as flaky - failures are logged but don't block)
|
||||||
|
# if ! ./tests/e2e/api/runners/run-newman-inference-tests.sh; then
|
||||||
|
# echo "WARNING: runners/run-newman-inference-tests.sh failed (flaky test - continuing)"
|
||||||
|
# fi
|
||||||
|
# if ! ./tests/e2e/api/run-all-integrations.sh; then
|
||||||
|
# echo "WARNING: run-all-integrations.sh failed (flaky test - continuing)"
|
||||||
|
# fi
|
||||||
|
# if ! ./tests/e2e/api/runners/run-newman-api-tests.sh; then
|
||||||
|
# echo "WARNING: run-newman-api-tests.sh failed (flaky test - continuing)"
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# echo ""
|
||||||
|
# echo "=== Docker image E2E API test passed for ${PLATFORM} ==="
|
||||||
180
.github/workflows/scripts/test-e2e-api.sh
vendored
Executable file
180
.github/workflows/scripts/test-e2e-api.sh
vendored
Executable file
@@ -0,0 +1,180 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# E2E API tests: /v1, /integrations, /api (Newman/Postman).
|
||||||
|
# Usage:
|
||||||
|
# ./test-e2e-api.sh # Bifrost already running at BIFROST_BASE_URL
|
||||||
|
# ./test-e2e-api.sh <bifrost-binary> [port] # Start Bifrost with config, then run tests
|
||||||
|
# Config: tests/integrations/python/config.json (merged with Postgres when starting server)
|
||||||
|
# Requires: Newman installed; provider API keys in environment when starting Bifrost
|
||||||
|
|
||||||
|
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||||
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
|
else
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
fi
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd -P)"
|
||||||
|
E2E_API_DIR="$REPO_ROOT/tests/e2e/api"
|
||||||
|
E2E_API_CONFIG="$REPO_ROOT/tests/integrations/python/config.json"
|
||||||
|
|
||||||
|
export BIFROST_BASE_URL="${BIFROST_BASE_URL:-http://localhost:8080}"
|
||||||
|
|
||||||
|
# ----- Optional: start Bifrost if binary path given -----
|
||||||
|
if [ -n "${1:-}" ]; then
|
||||||
|
BIFROST_BINARY="$1"
|
||||||
|
PORT="${2:-8080}"
|
||||||
|
POSTGRES_HOST="${POSTGRES_HOST:-localhost}"
|
||||||
|
POSTGRES_PORT="${POSTGRES_PORT:-5432}"
|
||||||
|
POSTGRES_USER="${POSTGRES_USER:-bifrost}"
|
||||||
|
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-bifrost_password}"
|
||||||
|
POSTGRES_DB="${POSTGRES_DB:-bifrost}"
|
||||||
|
POSTGRES_SSLMODE="${POSTGRES_SSLMODE:-disable}"
|
||||||
|
export POSTGRES_HOST POSTGRES_PORT POSTGRES_USER POSTGRES_PASSWORD POSTGRES_DB POSTGRES_SSLMODE
|
||||||
|
|
||||||
|
if [ ! -f "$BIFROST_BINARY" ] || [ ! -x "$BIFROST_BINARY" ]; then
|
||||||
|
echo "❌ Bifrost binary not found or not executable: $BIFROST_BINARY" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ ! -f "$E2E_API_CONFIG" ]; then
|
||||||
|
echo "❌ Config not found: $E2E_API_CONFIG" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
MERGED_CONFIG="$TEMP_DIR/config.json"
|
||||||
|
SERVER_LOG="$TEMP_DIR/server.log"
|
||||||
|
BIFROST_PID=""
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
local exit_code=$?
|
||||||
|
if [ -n "${BIFROST_PID:-}" ] && kill -0 "$BIFROST_PID" 2>/dev/null; then
|
||||||
|
kill "$BIFROST_PID" 2>/dev/null || true
|
||||||
|
wait "$BIFROST_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit $exit_code
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "📝 Merged config (providers + Postgres)..."
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
jq --arg host "$POSTGRES_HOST" --arg port "$POSTGRES_PORT" --arg user "$POSTGRES_USER" \
|
||||||
|
--arg pass "$POSTGRES_PASSWORD" --arg db "$POSTGRES_DB" --arg ssl "$POSTGRES_SSLMODE" \
|
||||||
|
'. + {
|
||||||
|
"config_store": {"enabled": true, "type": "postgres", "config": {"host": $host, "port": $port, "user": $user, "password": $pass, "db_name": $db, "ssl_mode": $ssl}},
|
||||||
|
"logs_store": {"enabled": true, "type": "postgres", "config": {"host": $host, "port": $port, "user": $user, "password": $pass, "db_name": $db, "ssl_mode": $ssl}}
|
||||||
|
}' "$E2E_API_CONFIG" > "$MERGED_CONFIG"
|
||||||
|
else
|
||||||
|
python3 - "$E2E_API_CONFIG" "$MERGED_CONFIG" << 'PYEOF'
|
||||||
|
import sys, json, os
|
||||||
|
with open(sys.argv[1]) as f: c = json.load(f)
|
||||||
|
pg = {"host": os.environ.get("POSTGRES_HOST", "localhost"), "port": os.environ.get("POSTGRES_PORT", "5432"), "user": os.environ.get("POSTGRES_USER", "bifrost"), "password": os.environ.get("POSTGRES_PASSWORD", "bifrost_password"), "db_name": os.environ.get("POSTGRES_DB", "bifrost"), "ssl_mode": os.environ.get("POSTGRES_SSLMODE", "disable")}
|
||||||
|
c["config_store"] = {"enabled": True, "type": "postgres", "config": pg}
|
||||||
|
c["logs_store"] = {"enabled": True, "type": "postgres", "config": dict(pg)}
|
||||||
|
with open(sys.argv[2], "w") as f: json.dump(c, f, indent=2)
|
||||||
|
PYEOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔄 Resetting PostgreSQL database..."
|
||||||
|
DOCKER_COMPOSE_FILE="$REPO_ROOT/.github/workflows/configs/docker-compose.yml"
|
||||||
|
if [ -f "$DOCKER_COMPOSE_FILE" ]; then
|
||||||
|
POSTGRES_CONTAINER=$(docker compose -f "$DOCKER_COMPOSE_FILE" ps -q postgres)
|
||||||
|
if [ -n "$POSTGRES_CONTAINER" ]; then
|
||||||
|
ESCAPED_DB_NAME="${POSTGRES_DB//\"/\"\"}"
|
||||||
|
docker exec "$POSTGRES_CONTAINER" psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d postgres -c "DROP DATABASE IF EXISTS \"$ESCAPED_DB_NAME\";" 2>/dev/null || true
|
||||||
|
docker exec "$POSTGRES_CONTAINER" psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d postgres -c "CREATE DATABASE \"$ESCAPED_DB_NAME\";" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🚀 Starting Bifrost on port $PORT..."
|
||||||
|
"$BIFROST_BINARY" --app-dir "$TEMP_DIR" --port "$PORT" --log-level debug > "$SERVER_LOG" 2>&1 &
|
||||||
|
BIFROST_PID=$!
|
||||||
|
|
||||||
|
MAX_WAIT=60
|
||||||
|
ELAPSED=0
|
||||||
|
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||||
|
if grep -q "successfully started bifrost" "$SERVER_LOG" 2>/dev/null; then
|
||||||
|
echo " ✅ Bifrost started"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if ! kill -0 "$BIFROST_PID" 2>/dev/null; then
|
||||||
|
echo " ❌ Bifrost process exited"
|
||||||
|
cat "$SERVER_LOG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
ELAPSED=$((ELAPSED + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $ELAPSED -ge $MAX_WAIT ]; then
|
||||||
|
echo " ❌ Bifrost did not start within ${MAX_WAIT}s"
|
||||||
|
cat "$SERVER_LOG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export BIFROST_BASE_URL="http://localhost:$PORT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----- Run tests (/v1, /integrations, /api) -----
|
||||||
|
echo ""
|
||||||
|
echo "🧪 Running E2E API tests (Newman)"
|
||||||
|
echo " BIFROST_BASE_URL=$BIFROST_BASE_URL"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if ! command -v newman &>/dev/null; then
|
||||||
|
echo "❌ Newman is not installed. Install with: npm install -g newman" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$E2E_API_DIR/setup-plugin.sh" ]; then
|
||||||
|
echo "📦 Setting up test plugin (optional)..."
|
||||||
|
"$E2E_API_DIR/setup-plugin.sh" 2>/dev/null || echo " Plugin setup skipped"
|
||||||
|
fi
|
||||||
|
if [ -f "$E2E_API_DIR/setup-mcp.sh" ]; then
|
||||||
|
echo "🔌 Setting up test MCP server (optional)..."
|
||||||
|
"$E2E_API_DIR/setup-mcp.sh" 2>/dev/null || echo " MCP setup skipped"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
cd "$E2E_API_DIR"
|
||||||
|
|
||||||
|
# In CI (e.g. GitHub Actions), generate HTML reports for artifact upload
|
||||||
|
REPORT_ARGS=""
|
||||||
|
if [ "${GITHUB_ACTIONS:-}" = "true" ] || [ "${CI:-0}" = "1" ]; then
|
||||||
|
REPORT_ARGS="--html"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Running /v1 test suite..."
|
||||||
|
echo "=========================================="
|
||||||
|
if ! ./runners/run-newman-inference-tests.sh $REPORT_ARGS; then
|
||||||
|
echo "❌ /v1 test suite failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Running /integrations test suites..."
|
||||||
|
echo "=========================================="
|
||||||
|
if ! ./runners/run-all-integration-tests.sh $REPORT_ARGS; then
|
||||||
|
echo "❌ /integrations test suites failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Running /api test suite..."
|
||||||
|
echo "=========================================="
|
||||||
|
if ! ./runners/run-newman-api-tests.sh $REPORT_ARGS; then
|
||||||
|
echo "❌ /api test suite failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Running inference features test suite..."
|
||||||
|
echo "=========================================="
|
||||||
|
if ! ./runners/run-newman-inference-features-tests.sh $REPORT_ARGS; then
|
||||||
|
echo "❌ inference features test suite failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ All E2E API tests passed (/v1, /integrations, /api, inference features)"
|
||||||
141
.github/workflows/scripts/test-e2e-ui.sh
vendored
Executable file
141
.github/workflows/scripts/test-e2e-ui.sh
vendored
Executable file
@@ -0,0 +1,141 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Test E2E UI with Playwright
|
||||||
|
# Usage: ./test-e2e-ui.sh
|
||||||
|
|
||||||
|
# Setup Go workspace for CI
|
||||||
|
source "$(dirname "$0")/setup-go-workspace.sh"
|
||||||
|
|
||||||
|
echo "🧪 Running E2E UI tests..."
|
||||||
|
|
||||||
|
CONFIGS_DIR=".github/workflows/configs"
|
||||||
|
|
||||||
|
# Cleanup function to ensure all services are stopped
|
||||||
|
cleanup() {
|
||||||
|
echo "🧹 Cleaning up..."
|
||||||
|
if [ -n "${SERVER_PID:-}" ]; then
|
||||||
|
kill "$SERVER_PID" 2>/dev/null || true
|
||||||
|
wait "$SERVER_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
rm -f "${SERVER_LOG:-}"
|
||||||
|
docker compose -f "$CONFIGS_DIR/docker-compose.yml" down 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Register cleanup handler to run on script exit (success or failure)
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Build UI
|
||||||
|
echo "🎨 Building UI..."
|
||||||
|
make build-ui
|
||||||
|
|
||||||
|
# Build bifrost-http binary
|
||||||
|
echo "🔨 Building bifrost-http binary..."
|
||||||
|
mkdir -p tmp
|
||||||
|
cd transports/bifrost-http
|
||||||
|
go build -o ../../tmp/bifrost-http .
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
# Start Docker services
|
||||||
|
echo "🐳 Starting Docker services (PostgreSQL, Redis, etc.)..."
|
||||||
|
docker compose -f "$CONFIGS_DIR/docker-compose.yml" up -d
|
||||||
|
|
||||||
|
# Wait for Docker services to be healthy with polling
|
||||||
|
echo "⏳ Waiting for Docker services to be ready..."
|
||||||
|
MAX_WAIT=300
|
||||||
|
ELAPSED=0
|
||||||
|
SERVICES_READY=false
|
||||||
|
|
||||||
|
# Get expected number of services
|
||||||
|
EXPECTED_SERVICES=$(docker compose -f "$CONFIGS_DIR/docker-compose.yml" config --services 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
|
||||||
|
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||||
|
# Get running container count
|
||||||
|
RUNNING_COUNT=$(docker compose -f "$CONFIGS_DIR/docker-compose.yml" ps --status running -q 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
|
||||||
|
# Check health status: count healthy and unhealthy (starting/unhealthy) services
|
||||||
|
HEALTH_OUTPUT=$(docker compose -f "$CONFIGS_DIR/docker-compose.yml" ps --format "{{.Name}}:{{.Health}}" 2>/dev/null)
|
||||||
|
HEALTHY_COUNT=$(echo "$HEALTH_OUTPUT" | grep -c ":healthy") || HEALTHY_COUNT=0
|
||||||
|
UNHEALTHY_COUNT=$(echo "$HEALTH_OUTPUT" | grep -cE ":(starting|unhealthy)") || UNHEALTHY_COUNT=0
|
||||||
|
|
||||||
|
# All services are ready when:
|
||||||
|
# 1. All expected services are running
|
||||||
|
# 2. No services are in "starting" or "unhealthy" state
|
||||||
|
if [ "$RUNNING_COUNT" -eq "$EXPECTED_SERVICES" ] && [ "$UNHEALTHY_COUNT" -eq "0" ]; then
|
||||||
|
SERVICES_READY=true
|
||||||
|
echo "✅ All Docker services are ready ($HEALTHY_COUNT with healthchecks, ${ELAPSED}s)"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
ELAPSED=$((ELAPSED + 2))
|
||||||
|
echo " ⏳ Waiting for services... ($RUNNING_COUNT/$EXPECTED_SERVICES running, $HEALTHY_COUNT healthy, $UNHEALTHY_COUNT starting, ${ELAPSED}s/${MAX_WAIT}s)"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$SERVICES_READY" = false ]; then
|
||||||
|
echo "❌ Docker services failed to become healthy within ${MAX_WAIT}s"
|
||||||
|
echo " Current service status:"
|
||||||
|
docker compose -f "$CONFIGS_DIR/docker-compose.yml" ps
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reset PostgreSQL database to clean state
|
||||||
|
echo "🧹 Resetting PostgreSQL database..."
|
||||||
|
docker exec -e PGPASSWORD=bifrost_password "$(docker compose -f "$CONFIGS_DIR/docker-compose.yml" ps -q postgres)" \
|
||||||
|
psql -U bifrost -d postgres \
|
||||||
|
-c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'bifrost' AND pid <> pg_backend_pid();" \
|
||||||
|
-c "DROP DATABASE IF EXISTS bifrost;" \
|
||||||
|
-c "CREATE DATABASE bifrost;"
|
||||||
|
|
||||||
|
# Start bifrost-http server with default config
|
||||||
|
SERVER_LOG=$(mktemp)
|
||||||
|
echo "🚀 Starting bifrost-http server..."
|
||||||
|
./tmp/bifrost-http --app-dir "$CONFIGS_DIR/default" --port 18080 --log-level debug 2>&1 | tee "$SERVER_LOG" &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
# Wait for server to be ready
|
||||||
|
echo "⏳ Waiting for server to start..."
|
||||||
|
MAX_WAIT=60
|
||||||
|
ELAPSED=0
|
||||||
|
SERVER_READY=false
|
||||||
|
|
||||||
|
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||||
|
if grep -q "successfully started bifrost, serving UI on http://localhost:18080" "$SERVER_LOG" 2>/dev/null; then
|
||||||
|
SERVER_READY=true
|
||||||
|
echo "✅ Server started successfully"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if server process is still running
|
||||||
|
if ! kill -0 $SERVER_PID 2>/dev/null; then
|
||||||
|
echo "❌ Server process died before starting"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
ELAPSED=$((ELAPSED + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$SERVER_READY" = false ]; then
|
||||||
|
echo "❌ Server failed to start within ${MAX_WAIT}s"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install Playwright dependencies
|
||||||
|
echo "📦 Installing Playwright dependencies..."
|
||||||
|
cd tests/e2e
|
||||||
|
npm ci
|
||||||
|
npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
# Run Playwright tests (BASE_URL = browser; BIFROST_BASE_URL = global-setup API calls).
|
||||||
|
# Forward MCP_SSE_HEADERS so the mcp-registry SSE test can use it (set in workflow env).
|
||||||
|
echo "🎭 Running Playwright E2E tests..."
|
||||||
|
CI=true SKIP_WEB_SERVER=1 BASE_URL=http://localhost:18080 BIFROST_BASE_URL=http://localhost:18080 \
|
||||||
|
MCP_SSE_HEADERS="${MCP_SSE_HEADERS:-}" \
|
||||||
|
npx playwright test --workers=4
|
||||||
|
PLAYWRIGHT_EXIT=$?
|
||||||
|
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
echo "✅ E2E UI tests completed"
|
||||||
|
exit $PLAYWRIGHT_EXIT
|
||||||
61
.github/workflows/scripts/test-framework.sh
vendored
Executable file
61
.github/workflows/scripts/test-framework.sh
vendored
Executable file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Test framework component
|
||||||
|
# Usage: ./test-framework.sh
|
||||||
|
|
||||||
|
# Setup Go workspace for CI
|
||||||
|
source "$(dirname "$0")/setup-go-workspace.sh"
|
||||||
|
|
||||||
|
echo "🧪 Running framework tests..."
|
||||||
|
|
||||||
|
# Cleanup function to ensure Docker services are stopped
|
||||||
|
cleanup_docker() {
|
||||||
|
echo "🧹 Cleaning up Docker services..."
|
||||||
|
if command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
docker-compose -f tests/docker-compose.yml down 2>/dev/null || true
|
||||||
|
elif docker compose version >/dev/null 2>&1; then
|
||||||
|
docker compose -f tests/docker-compose.yml down 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Register cleanup handler to run on script exit (success or failure)
|
||||||
|
trap cleanup_docker EXIT
|
||||||
|
|
||||||
|
# Starting dependencies of framework tests
|
||||||
|
echo "🔧 Starting dependencies of framework tests..."
|
||||||
|
# Use docker compose (v2) if available, fallback to docker-compose (v1)
|
||||||
|
if command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
docker-compose -f tests/docker-compose.yml up -d
|
||||||
|
elif docker compose version >/dev/null 2>&1; then
|
||||||
|
docker compose -f tests/docker-compose.yml up -d
|
||||||
|
else
|
||||||
|
echo "❌ Neither docker-compose nor docker compose is available"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 20
|
||||||
|
|
||||||
|
# Validate framework build
|
||||||
|
echo "🔨 Validating framework build..."
|
||||||
|
cd framework
|
||||||
|
go build ./...
|
||||||
|
echo "✅ Framework build validation successful"
|
||||||
|
|
||||||
|
# Run framework tests with coverage
|
||||||
|
echo "🧪 Running framework tests with coverage..."
|
||||||
|
go test --race -coverprofile=coverage.txt -coverpkg=./... ./...
|
||||||
|
|
||||||
|
# Upload coverage to Codecov
|
||||||
|
if [ -n "${CODECOV_TOKEN:-}" ]; then
|
||||||
|
echo "📊 Uploading coverage to Codecov..."
|
||||||
|
curl -Os https://uploader.codecov.io/latest/linux/codecov
|
||||||
|
chmod +x codecov
|
||||||
|
./codecov -t "$CODECOV_TOKEN" -f coverage.txt -F framework
|
||||||
|
rm -f codecov coverage.txt
|
||||||
|
else
|
||||||
|
echo "ℹ️ CODECOV_TOKEN not set, skipping coverage upload"
|
||||||
|
rm -f coverage.txt
|
||||||
|
fi
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
echo "✅ Framework tests completed successfully"
|
||||||
183
.github/workflows/scripts/test-integrations.sh
vendored
Executable file
183
.github/workflows/scripts/test-integrations.sh
vendored
Executable file
@@ -0,0 +1,183 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Test integration tests by building bifrost-http from source, starting it,
|
||||||
|
# and running Python and TypeScript SDK integration tests
|
||||||
|
# Usage: ./test-integrations.sh
|
||||||
|
|
||||||
|
# Get the absolute path of the script directory
|
||||||
|
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||||
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
|
else
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Repository root (3 levels up from .github/workflows/scripts)
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd -P)"
|
||||||
|
|
||||||
|
# Setup Go workspace for CI (go.work is gitignored, must be regenerated)
|
||||||
|
source "$SCRIPT_DIR/setup-go-workspace.sh"
|
||||||
|
|
||||||
|
echo "🧪 Running Integration Tests"
|
||||||
|
echo " Repository root: $REPO_ROOT"
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
TEST_PORT="${PORT:-8080}"
|
||||||
|
TEST_HOST="${HOST:-localhost}"
|
||||||
|
BIFROST_PID=""
|
||||||
|
TEST_FAILED=0
|
||||||
|
LOG_FILE="$(mktemp /tmp/bifrost-integrations.XXXXXX.log)"
|
||||||
|
|
||||||
|
# Cleanup function
|
||||||
|
cleanup() {
|
||||||
|
local exit_code=$?
|
||||||
|
echo ""
|
||||||
|
echo "🧹 Cleaning up..."
|
||||||
|
|
||||||
|
# Kill Bifrost server if running
|
||||||
|
if [ -n "${BIFROST_PID:-}" ]; then
|
||||||
|
echo " Stopping Bifrost server (PID: $BIFROST_PID)..."
|
||||||
|
kill "$BIFROST_PID" 2>/dev/null || true
|
||||||
|
wait "$BIFROST_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "${LOG_FILE:-}" 2>/dev/null || true
|
||||||
|
|
||||||
|
exit $exit_code
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Step 1: Build bifrost-http from source
|
||||||
|
echo ""
|
||||||
|
echo "🔨 Building bifrost-http from source..."
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
# Build the UI first, then the binary
|
||||||
|
make build-ui
|
||||||
|
make build
|
||||||
|
|
||||||
|
if [ ! -f "$REPO_ROOT/tmp/bifrost-http" ]; then
|
||||||
|
echo "❌ Error: bifrost-http binary not found at $REPO_ROOT/tmp/bifrost-http"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Build complete: $REPO_ROOT/tmp/bifrost-http"
|
||||||
|
|
||||||
|
# Step 2: Start Bifrost server with Python integration test config
|
||||||
|
echo ""
|
||||||
|
echo "🚀 Starting Bifrost server..."
|
||||||
|
echo " Config: tests/integrations/python/config.json"
|
||||||
|
echo " Host: $TEST_HOST"
|
||||||
|
echo " Port: $TEST_PORT"
|
||||||
|
|
||||||
|
# Start server in background with Python config directory
|
||||||
|
"$REPO_ROOT/tmp/bifrost-http" \
|
||||||
|
-host "$TEST_HOST" \
|
||||||
|
-port "$TEST_PORT" \
|
||||||
|
-log-style json \
|
||||||
|
-log-level info \
|
||||||
|
-app-dir "$REPO_ROOT/tests/integrations/python" \
|
||||||
|
> "$LOG_FILE" 2>&1 &
|
||||||
|
|
||||||
|
BIFROST_PID=$!
|
||||||
|
echo " Started with PID: $BIFROST_PID"
|
||||||
|
|
||||||
|
|
||||||
|
# Wait for server to be ready
|
||||||
|
echo "⏳ Waiting for Bifrost to be ready..."
|
||||||
|
MAX_WAIT=30
|
||||||
|
ELAPSED=0
|
||||||
|
SERVER_READY=false
|
||||||
|
|
||||||
|
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||||
|
if curl --connect-timeout 10 --max-time 20 -sf "http://$TEST_HOST:$TEST_PORT/health" > /dev/null 2>&1; then
|
||||||
|
SERVER_READY=true
|
||||||
|
echo "✅ Bifrost is ready (took ${ELAPSED}s)"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if server process is still running
|
||||||
|
if ! kill -0 "$BIFROST_PID" 2>/dev/null; then
|
||||||
|
echo "❌ Bifrost process died unexpectedly"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
ELAPSED=$((ELAPSED + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$SERVER_READY" = false ]; then
|
||||||
|
echo "❌ Bifrost failed to start within ${MAX_WAIT}s"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set environment variable for tests
|
||||||
|
export BIFROST_BASE_URL="http://$TEST_HOST:$TEST_PORT"
|
||||||
|
echo " BIFROST_BASE_URL=$BIFROST_BASE_URL"
|
||||||
|
|
||||||
|
# Step 3: Run Python integration tests
|
||||||
|
echo ""
|
||||||
|
echo "🐍 Running Python integration tests..."
|
||||||
|
echo "="
|
||||||
|
|
||||||
|
cd "$REPO_ROOT/tests/integrations/python"
|
||||||
|
|
||||||
|
# Check if uv is available
|
||||||
|
if command -v uv >/dev/null 2>&1; then
|
||||||
|
echo "📦 Installing Python dependencies with uv..."
|
||||||
|
uv sync --frozen --quiet
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🏃 Running Python tests..."
|
||||||
|
if ! uv run pytest -v --tb=short; then
|
||||||
|
echo "⚠️ Python tests failed"
|
||||||
|
TEST_FAILED=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⚠️ uv not found, trying pip..."
|
||||||
|
|
||||||
|
# Create virtual environment if needed
|
||||||
|
if [ ! -d ".venv" ]; then
|
||||||
|
python3 -m venv .venv
|
||||||
|
fi
|
||||||
|
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -q -e .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🏃 Running Python tests..."
|
||||||
|
if ! pytest -v --tb=short; then
|
||||||
|
echo "⚠️ Python tests failed"
|
||||||
|
TEST_FAILED=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 4: Run TypeScript integration tests
|
||||||
|
echo ""
|
||||||
|
echo "📘 Running TypeScript integration tests..."
|
||||||
|
echo "="
|
||||||
|
|
||||||
|
cd "$REPO_ROOT/tests/integrations/typescript"
|
||||||
|
|
||||||
|
# Install dependencies if needed
|
||||||
|
if [ ! -d "node_modules" ]; then
|
||||||
|
echo "📦 Installing TypeScript dependencies with npm..."
|
||||||
|
npm ci
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🏃 Running TypeScript tests..."
|
||||||
|
if ! npm test; then
|
||||||
|
echo "⚠️ TypeScript tests failed"
|
||||||
|
TEST_FAILED=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo ""
|
||||||
|
echo "="
|
||||||
|
if [ $TEST_FAILED -eq 1 ]; then
|
||||||
|
echo "❌ Some integration tests failed"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "✅ All integration tests passed!"
|
||||||
|
fi
|
||||||
85
.github/workflows/scripts/upload-cli-to-r2.sh
vendored
Executable file
85
.github/workflows/scripts/upload-cli-to-r2.sh
vendored
Executable file
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Upload CLI builds to R2 with retry logic
|
||||||
|
# Usage: ./upload-cli-to-r2.sh <cli-version>
|
||||||
|
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
echo "Usage: $0 <cli-version> (e.g., cli/v1.0.0)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
CLI_TAG="$1"
|
||||||
|
# Validate tag format: must be cli/vX.Y.Z or cli/vX.Y.Z-prerelease
|
||||||
|
if [[ ! "$CLI_TAG" =~ ^cli/v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
|
||||||
|
echo "❌ Invalid tag format: $CLI_TAG"
|
||||||
|
echo " Expected format: cli/vX.Y.Z or cli/vX.Y.Z-prerelease (e.g., cli/v1.0.0, cli/v1.0.0-rc.1)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -d "./dist" ]]; then
|
||||||
|
echo "❌ ./dist not found. Build artifacts must be present before upload."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
: "${R2_ENDPOINT:?R2_ENDPOINT env var is required}"
|
||||||
|
: "${R2_BUCKET:?R2_BUCKET env var is required}"
|
||||||
|
|
||||||
|
# Strip 'cli/' prefix from version
|
||||||
|
VERSION_ONLY=${CLI_TAG#cli/v}
|
||||||
|
CLI_VERSION="v${VERSION_ONLY}"
|
||||||
|
printf '%s' "$CLI_VERSION" > "./dist/version.txt"
|
||||||
|
R2_ENDPOINT="$(echo "$R2_ENDPOINT" | tr -d '[:space:]')"
|
||||||
|
|
||||||
|
echo "📤 Uploading CLI binaries for version: $CLI_VERSION"
|
||||||
|
|
||||||
|
# Function to upload with retry
|
||||||
|
upload_with_retry() {
|
||||||
|
local source_path="$1"
|
||||||
|
local dest_path="$2"
|
||||||
|
local max_retries=3
|
||||||
|
|
||||||
|
for attempt in $(seq 1 $max_retries); do
|
||||||
|
echo "🔄 Attempt $attempt/$max_retries: Uploading to $dest_path"
|
||||||
|
|
||||||
|
if aws s3 sync "$source_path" "$dest_path" \
|
||||||
|
--endpoint-url "$R2_ENDPOINT" \
|
||||||
|
--profile "${R2_AWS_PROFILE:-R2}" \
|
||||||
|
--no-progress \
|
||||||
|
--delete; then
|
||||||
|
echo "✅ Upload successful to $dest_path"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "⚠️ Attempt $attempt failed"
|
||||||
|
if [ $attempt -lt $max_retries ]; then
|
||||||
|
delay=$((2 ** attempt))
|
||||||
|
echo "🕐 Waiting ${delay}s before retry..."
|
||||||
|
sleep $delay
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "❌ All $max_retries attempts failed for $dest_path"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upload to versioned path
|
||||||
|
if ! upload_with_retry "./dist/" "s3://$R2_BUCKET/bifrost-cli/$CLI_VERSION/"; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if this is a prerelease version (semver: presence of a hyphen denotes pre-release)
|
||||||
|
if [[ "$CLI_VERSION" == *-* ]]; then
|
||||||
|
echo "🔍 Detected prerelease version: $CLI_VERSION"
|
||||||
|
echo "⏭️ Skipping upload to latest/ for prerelease"
|
||||||
|
else
|
||||||
|
echo "🔍 Detected stable release: $CLI_VERSION"
|
||||||
|
|
||||||
|
# Small delay between uploads (configurable; default 2s)
|
||||||
|
sleep "${INTER_UPLOAD_SLEEP_SECONDS:-2}"
|
||||||
|
|
||||||
|
# Upload to latest path
|
||||||
|
echo "📤 Uploading to latest/"
|
||||||
|
if ! upload_with_retry "./dist/" "s3://$R2_BUCKET/bifrost-cli/latest/"; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🎉 All CLI binaries uploaded successfully to R2"
|
||||||
95
.github/workflows/scripts/upload-to-r2.sh
vendored
Executable file
95
.github/workflows/scripts/upload-to-r2.sh
vendored
Executable file
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Upload builds to R2 with retry logic
|
||||||
|
# Usage: ./upload-to-r2.sh <transport-version>
|
||||||
|
#
|
||||||
|
# Environment variables:
|
||||||
|
# R2_ENDPOINT - Required. R2 endpoint URL
|
||||||
|
# R2_BUCKET - Required. R2 bucket name
|
||||||
|
# R2_AWS_PROFILE - Optional. AWS CLI profile (default: R2)
|
||||||
|
# SKIP_LATEST_UPLOAD - Optional. Set to "true" to skip latest/ upload
|
||||||
|
# (used when multiple jobs upload in parallel and
|
||||||
|
# the finalize job handles latest/ separately)
|
||||||
|
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
echo "Usage: $0 <transport-version> (e.g., transports/v1.2.3)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
TRANSPORT_VERSION="$1"
|
||||||
|
if [[ ! -d "./dist" ]]; then
|
||||||
|
echo "❌ ./dist not found. Build artifacts must be present before upload."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
: "${R2_ENDPOINT:?R2_ENDPOINT env var is required}"
|
||||||
|
: "${R2_BUCKET:?R2_BUCKET env var is required}"
|
||||||
|
|
||||||
|
# Strip 'transports/' prefix from version
|
||||||
|
VERSION_ONLY=${TRANSPORT_VERSION#transports/v}
|
||||||
|
CLI_VERSION="v${VERSION_ONLY}"
|
||||||
|
R2_ENDPOINT="$(echo "$R2_ENDPOINT" | tr -d '[:space:]')"
|
||||||
|
|
||||||
|
echo "📤 Uploading binaries for version: $CLI_VERSION"
|
||||||
|
|
||||||
|
# Function to upload with retry
|
||||||
|
# Uses aws s3 cp --recursive instead of s3 sync --delete so that
|
||||||
|
# parallel upload jobs don't wipe each other's files.
|
||||||
|
upload_with_retry() {
|
||||||
|
local source_path="$1"
|
||||||
|
local dest_path="$2"
|
||||||
|
local max_retries=3
|
||||||
|
|
||||||
|
for attempt in $(seq 1 $max_retries); do
|
||||||
|
echo "🔄 Attempt $attempt/$max_retries: Uploading to $dest_path"
|
||||||
|
|
||||||
|
if aws s3 cp "$source_path" "$dest_path" \
|
||||||
|
--endpoint-url "$R2_ENDPOINT" \
|
||||||
|
--profile "${R2_AWS_PROFILE:-R2}" \
|
||||||
|
--no-progress \
|
||||||
|
--recursive; then
|
||||||
|
echo "✅ Upload successful to $dest_path"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "⚠️ Attempt $attempt failed"
|
||||||
|
if [ $attempt -lt $max_retries ]; then
|
||||||
|
delay=$((2 ** attempt))
|
||||||
|
echo "🕐 Waiting ${delay}s before retry..."
|
||||||
|
sleep $delay
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "❌ All $max_retries attempts failed for $dest_path"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upload to versioned path
|
||||||
|
if ! upload_with_retry "./dist/" "s3://$R2_BUCKET/bifrost/$CLI_VERSION/"; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Skip latest/ upload if requested (finalize job handles it after all builds complete)
|
||||||
|
if [[ "${SKIP_LATEST_UPLOAD:-false}" == "true" ]]; then
|
||||||
|
echo "⏭️ Skipping latest/ upload (will be handled by finalize job)"
|
||||||
|
echo "🎉 Binaries uploaded successfully to R2 (versioned path)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if this is a prerelease version (semver: presence of a hyphen denotes pre-release)
|
||||||
|
if [[ "$CLI_VERSION" == *-* ]]; then
|
||||||
|
echo "🔍 Detected prerelease version: $CLI_VERSION"
|
||||||
|
echo "⏭️ Skipping upload to latest/ for prerelease"
|
||||||
|
else
|
||||||
|
echo "🔍 Detected stable release: $CLI_VERSION"
|
||||||
|
|
||||||
|
# Small delay between uploads (configurable; default 2s)
|
||||||
|
sleep "${INTER_UPLOAD_SLEEP_SECONDS:-2}"
|
||||||
|
|
||||||
|
# Upload to latest path
|
||||||
|
echo "📤 Uploading to latest/"
|
||||||
|
if ! upload_with_retry "./dist/" "s3://$R2_BUCKET/bifrost/latest/"; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🎉 All binaries uploaded successfully to R2"
|
||||||
201
.github/workflows/scripts/validate-go-config-fields.sh
vendored
Executable file
201
.github/workflows/scripts/validate-go-config-fields.sh
vendored
Executable file
@@ -0,0 +1,201 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Validate that config.schema.json stays in sync with Go struct JSON tags
|
||||||
|
# Extracts json:"..." tags from Go structs and compares against schema properties
|
||||||
|
|
||||||
|
echo "🔍 Validating Go struct fields vs config.schema.json..."
|
||||||
|
echo "========================================================"
|
||||||
|
|
||||||
|
# Get the repository root
|
||||||
|
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||||
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
|
else
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
fi
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||||
|
|
||||||
|
CONFIG_SCHEMA="$REPO_ROOT/transports/config.schema.json"
|
||||||
|
|
||||||
|
# Color codes
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
ERRORS=0
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
# Check prerequisites
|
||||||
|
if [ ! -f "$CONFIG_SCHEMA" ]; then
|
||||||
|
echo "❌ Config schema not found: $CONFIG_SCHEMA"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v jq &> /dev/null; then
|
||||||
|
echo "❌ jq is required for Go-to-schema validation"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract JSON tags from a Go struct
|
||||||
|
# Usage: extract_go_json_tags <file> <struct_name>
|
||||||
|
# Returns sorted list of json tag names (excluding "-" and ",omitempty" suffixes)
|
||||||
|
extract_go_json_tags() {
|
||||||
|
local file=$1
|
||||||
|
local struct_name=$2
|
||||||
|
awk "/^type ${struct_name} struct/,/^}/" "$file" \
|
||||||
|
| grep -oE 'json:"([^"]+)"' \
|
||||||
|
| sed 's/json:"//;s/"//' \
|
||||||
|
| sed 's/,.*//' \
|
||||||
|
| grep -v '^-$' \
|
||||||
|
| sort
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract property keys from config.schema.json at a given jq path
|
||||||
|
# Usage: extract_schema_keys <jq_path>
|
||||||
|
extract_schema_keys() {
|
||||||
|
local jq_path=$1
|
||||||
|
jq -r "${jq_path} | keys[]" "$CONFIG_SCHEMA" 2>/dev/null | sort
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compare Go struct tags against schema properties
|
||||||
|
# Usage: compare_struct_to_schema <label> <go_file> <struct_name> <jq_path> <exclusions...>
|
||||||
|
compare_struct_to_schema() {
|
||||||
|
local label=$1
|
||||||
|
local go_file=$2
|
||||||
|
local struct_name=$3
|
||||||
|
local jq_path=$4
|
||||||
|
shift 4
|
||||||
|
local exclusions=("$@")
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN} Checking: $label ($struct_name)${NC}"
|
||||||
|
|
||||||
|
if [ ! -f "$go_file" ]; then
|
||||||
|
echo -e "${RED} ❌ Go file not found: $go_file${NC}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local go_tags
|
||||||
|
go_tags=$(extract_go_json_tags "$go_file" "$struct_name")
|
||||||
|
|
||||||
|
local schema_keys
|
||||||
|
schema_keys=$(extract_schema_keys "$jq_path")
|
||||||
|
|
||||||
|
if [ -z "$go_tags" ]; then
|
||||||
|
echo -e "${RED} ❌ No JSON tags found for struct $struct_name in $go_file${NC}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$schema_keys" ]; then
|
||||||
|
echo -e "${RED} ❌ No properties found at $jq_path in config.schema.json${NC}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local has_error=false
|
||||||
|
|
||||||
|
# Check Go fields missing from schema
|
||||||
|
while IFS= read -r tag; do
|
||||||
|
[ -z "$tag" ] && continue
|
||||||
|
# Check if excluded
|
||||||
|
local excluded=false
|
||||||
|
for exc in "${exclusions[@]+"${exclusions[@]}"}"; do
|
||||||
|
if [ "$tag" = "$exc" ]; then
|
||||||
|
excluded=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$excluded" = "true" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! echo "$schema_keys" | grep -qx "$tag"; then
|
||||||
|
echo -e "${RED} ❌ Go field '$tag' missing from schema ($jq_path)${NC}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
has_error=true
|
||||||
|
fi
|
||||||
|
done <<< "$go_tags"
|
||||||
|
|
||||||
|
# Check schema fields missing from Go (warnings only)
|
||||||
|
while IFS= read -r key; do
|
||||||
|
[ -z "$key" ] && continue
|
||||||
|
if ! echo "$go_tags" | grep -qx "$key"; then
|
||||||
|
echo -e "${YELLOW} ⚠️ Schema property '$key' not found in Go struct $struct_name${NC}"
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
fi
|
||||||
|
done <<< "$schema_keys"
|
||||||
|
|
||||||
|
if [ "$has_error" = "false" ]; then
|
||||||
|
echo -e "${GREEN} ✅ All Go fields present in schema${NC}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Comparing Go struct JSON tags against config.schema.json properties..."
|
||||||
|
|
||||||
|
# ClientConfig — framework/configstore/clientconfig.go → .properties.client.properties
|
||||||
|
compare_struct_to_schema \
|
||||||
|
"Client Config" \
|
||||||
|
"$REPO_ROOT/framework/configstore/clientconfig.go" \
|
||||||
|
"ClientConfig" \
|
||||||
|
'.properties.client.properties'
|
||||||
|
|
||||||
|
# GovernanceConfig — framework/configstore/clientconfig.go → .properties.governance.properties
|
||||||
|
compare_struct_to_schema \
|
||||||
|
"Governance Config" \
|
||||||
|
"$REPO_ROOT/framework/configstore/clientconfig.go" \
|
||||||
|
"GovernanceConfig" \
|
||||||
|
'.properties.governance.properties'
|
||||||
|
|
||||||
|
# MCPConfig — core/schemas/mcp.go → .properties.mcp.properties
|
||||||
|
compare_struct_to_schema \
|
||||||
|
"MCP Config" \
|
||||||
|
"$REPO_ROOT/core/schemas/mcp.go" \
|
||||||
|
"MCPConfig" \
|
||||||
|
'.properties.mcp.properties'
|
||||||
|
|
||||||
|
# MCPToolManagerConfig — core/schemas/mcp.go → .$defs.mcp_tool_manager_config.properties
|
||||||
|
compare_struct_to_schema \
|
||||||
|
"MCP Tool Manager Config" \
|
||||||
|
"$REPO_ROOT/core/schemas/mcp.go" \
|
||||||
|
"MCPToolManagerConfig" \
|
||||||
|
'."$defs".mcp_tool_manager_config.properties'
|
||||||
|
|
||||||
|
# MCPClientConfig — core/schemas/mcp.go → .$defs.mcp_client_config.properties
|
||||||
|
# Exclude: state (runtime-only), config_hash (internal)
|
||||||
|
compare_struct_to_schema \
|
||||||
|
"MCP Client Config" \
|
||||||
|
"$REPO_ROOT/core/schemas/mcp.go" \
|
||||||
|
"MCPClientConfig" \
|
||||||
|
'."$defs".mcp_client_config.properties' \
|
||||||
|
"state" \
|
||||||
|
"config_hash"
|
||||||
|
|
||||||
|
# PluginConfig — core/schemas/plugin.go → .properties.plugins.items.properties
|
||||||
|
compare_struct_to_schema \
|
||||||
|
"Plugin Config" \
|
||||||
|
"$REPO_ROOT/core/schemas/plugin.go" \
|
||||||
|
"PluginConfig" \
|
||||||
|
'.properties.plugins.items.properties'
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo ""
|
||||||
|
echo "========================================================"
|
||||||
|
echo "🏁 Go-to-Schema Validation Complete!"
|
||||||
|
echo "========================================================"
|
||||||
|
echo -e "${GREEN}Errors: $ERRORS${NC}"
|
||||||
|
echo -e "${YELLOW}Warnings: $WARNINGS${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
echo -e "${RED}❌ Some Go struct fields are missing from config.schema.json.${NC}"
|
||||||
|
echo " Add the missing properties to transports/config.schema.json"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✅ All Go struct fields are present in config.schema.json!${NC}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
1300
.github/workflows/scripts/validate-helm-config-fields.sh
vendored
Executable file
1300
.github/workflows/scripts/validate-helm-config-fields.sh
vendored
Executable file
File diff suppressed because it is too large
Load Diff
664
.github/workflows/scripts/validate-helm-schema.sh
vendored
Executable file
664
.github/workflows/scripts/validate-helm-schema.sh
vendored
Executable file
@@ -0,0 +1,664 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Validate that the Helm chart values.schema.json is in sync with config.schema.json
|
||||||
|
# This script extracts required fields from both schemas and compares them
|
||||||
|
|
||||||
|
# Get the repository root
|
||||||
|
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||||
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
|
else
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
fi
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||||
|
|
||||||
|
CONFIG_SCHEMA="$REPO_ROOT/transports/config.schema.json"
|
||||||
|
HELM_SCHEMA="$REPO_ROOT/helm-charts/bifrost/values.schema.json"
|
||||||
|
|
||||||
|
echo "📋 Comparing schemas:"
|
||||||
|
echo " Config schema: $CONFIG_SCHEMA"
|
||||||
|
echo " Helm schema: $HELM_SCHEMA"
|
||||||
|
|
||||||
|
# Check if files exist
|
||||||
|
if [ ! -f "$CONFIG_SCHEMA" ]; then
|
||||||
|
echo "❌ Config schema not found: $CONFIG_SCHEMA"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$HELM_SCHEMA" ]; then
|
||||||
|
echo "❌ Helm schema not found: $HELM_SCHEMA"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if jq is available
|
||||||
|
if ! command -v jq &> /dev/null; then
|
||||||
|
echo "⚠️ jq not found, skipping detailed schema comparison"
|
||||||
|
echo " Install jq for full schema validation"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
# Function to extract required fields from a schema definition
|
||||||
|
extract_required_fields() {
|
||||||
|
local schema_file="$1"
|
||||||
|
local def_path="$2"
|
||||||
|
jq -r "$def_path.required // [] | .[]" "$schema_file" 2>/dev/null | sort
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check if a definition exists in schema
|
||||||
|
def_exists() {
|
||||||
|
local schema_file="$1"
|
||||||
|
local def_path="$2"
|
||||||
|
jq -e "$def_path" "$schema_file" > /dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Checking required fields in governance entities..."
|
||||||
|
|
||||||
|
# Check governance.budgets required fields
|
||||||
|
CONFIG_BUDGET_REQUIRED=$(jq -r '.properties.governance.properties.budgets.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_BUDGET_REQUIRED=$(jq -r '.properties.bifrost.properties.governance.properties.budgets.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_BUDGET_REQUIRED" != "$HELM_BUDGET_REQUIRED" ]; then
|
||||||
|
echo "❌ Budget required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_BUDGET_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_BUDGET_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Budget required fields match: [$CONFIG_BUDGET_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check governance.rate_limits required fields
|
||||||
|
CONFIG_RATELIMIT_REQUIRED=$(jq -r '.properties.governance.properties.rate_limits.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_RATELIMIT_REQUIRED=$(jq -r '.properties.bifrost.properties.governance.properties.rateLimits.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_RATELIMIT_REQUIRED" != "$HELM_RATELIMIT_REQUIRED" ]; then
|
||||||
|
echo "❌ Rate limits required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_RATELIMIT_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_RATELIMIT_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Rate limits required fields match: [$CONFIG_RATELIMIT_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check governance.customers required fields
|
||||||
|
CONFIG_CUSTOMER_REQUIRED=$(jq -r '.properties.governance.properties.customers.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_CUSTOMER_REQUIRED=$(jq -r '.properties.bifrost.properties.governance.properties.customers.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_CUSTOMER_REQUIRED" != "$HELM_CUSTOMER_REQUIRED" ]; then
|
||||||
|
echo "❌ Customer required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_CUSTOMER_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_CUSTOMER_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Customer required fields match: [$CONFIG_CUSTOMER_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check governance.teams required fields
|
||||||
|
CONFIG_TEAM_REQUIRED=$(jq -r '.properties.governance.properties.teams.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_TEAM_REQUIRED=$(jq -r '.properties.bifrost.properties.governance.properties.teams.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_TEAM_REQUIRED" != "$HELM_TEAM_REQUIRED" ]; then
|
||||||
|
echo "❌ Team required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_TEAM_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_TEAM_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Team required fields match: [$CONFIG_TEAM_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check governance.virtual_keys required fields
|
||||||
|
CONFIG_VK_REQUIRED=$(jq -r '.properties.governance.properties.virtual_keys.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_VK_REQUIRED=$(jq -r '.properties.bifrost.properties.governance.properties.virtualKeys.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_VK_REQUIRED" != "$HELM_VK_REQUIRED" ]; then
|
||||||
|
echo "❌ Virtual key required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_VK_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_VK_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Virtual key required fields match: [$CONFIG_VK_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo '🔍 Checking required fields in $defs...'
|
||||||
|
|
||||||
|
# Check base_key required fields
|
||||||
|
CONFIG_BASEKEY_REQUIRED=$(jq -r '."$defs".base_key.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_BASEKEY_REQUIRED=$(jq -r '."$defs".providerKey.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_BASEKEY_REQUIRED" != "$HELM_BASEKEY_REQUIRED" ]; then
|
||||||
|
echo "❌ Provider key (base_key) required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_BASEKEY_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_BASEKEY_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Provider key required fields match: [$CONFIG_BASEKEY_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check azure_key_config required fields
|
||||||
|
CONFIG_AZURE_REQUIRED=$(jq -r '."$defs".azure_key.allOf[1].properties.azure_key_config.properties | keys | map(select(. as $k | ["endpoint", "api_version"] | index($k))) | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "endpoint,api_version")
|
||||||
|
HELM_AZURE_REQUIRED=$(jq -r '."$defs".providerKey.properties.azure_key_config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Normalize the comparison (config schema uses allOf pattern)
|
||||||
|
CONFIG_AZURE_REQ_NORM=$(jq -r '."$defs".azure_key.allOf[1].properties.azure_key_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
if [ -z "$CONFIG_AZURE_REQ_NORM" ]; then
|
||||||
|
# Try the direct path in $defs
|
||||||
|
CONFIG_AZURE_REQ_NORM=$(jq -r '."$defs".azure_key_config.required // ["endpoint", "api_version"] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "api_version,endpoint")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$CONFIG_AZURE_REQ_NORM" != "$HELM_AZURE_REQUIRED" ]; then
|
||||||
|
echo "❌ Azure key config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_AZURE_REQ_NORM]"
|
||||||
|
echo " Helm: [$HELM_AZURE_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Azure key config required fields match: [$HELM_AZURE_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check vertex_key_config required fields
|
||||||
|
CONFIG_VERTEX_REQUIRED=$(jq -r '."$defs".vertex_key.allOf[1].properties.vertex_key_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_VERTEX_REQUIRED=$(jq -r '."$defs".providerKey.properties.vertex_key_config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_VERTEX_REQUIRED" != "$HELM_VERTEX_REQUIRED" ]; then
|
||||||
|
echo "❌ Vertex key config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_VERTEX_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_VERTEX_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Vertex key config required fields match: [$CONFIG_VERTEX_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check bedrock_key_config required fields
|
||||||
|
CONFIG_BEDROCK_REQUIRED=$(jq -r '."$defs".bedrock_key.allOf[1].properties.bedrock_key_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_BEDROCK_REQUIRED=$(jq -r '."$defs".providerKey.properties.bedrock_key_config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_BEDROCK_REQUIRED" != "$HELM_BEDROCK_REQUIRED" ]; then
|
||||||
|
echo "❌ Bedrock key config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_BEDROCK_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_BEDROCK_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Bedrock key config required fields match: [$CONFIG_BEDROCK_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check vllm_key_config required fields
|
||||||
|
CONFIG_VLLM_REQUIRED=$(jq -r '."$defs".vllm_key.allOf[1].properties.vllm_key_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_VLLM_REQUIRED=$(jq -r '."$defs".providerKey.properties.vllm_key_config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_VLLM_REQUIRED" != "$HELM_VLLM_REQUIRED" ]; then
|
||||||
|
echo "❌ VLLM key config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_VLLM_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_VLLM_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ VLLM key config required fields match: [$HELM_VLLM_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check concurrency_and_buffer_size required fields (renamed from concurrency_config)
|
||||||
|
CONFIG_CONCURRENCY_REQUIRED=$(jq -r '."$defs".concurrency_and_buffer_size.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_CONCURRENCY_REQUIRED=$(jq -r '."$defs".concurrencyConfig.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_CONCURRENCY_REQUIRED" != "$HELM_CONCURRENCY_REQUIRED" ]; then
|
||||||
|
echo "❌ Concurrency config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_CONCURRENCY_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_CONCURRENCY_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Concurrency config required fields match: [$CONFIG_CONCURRENCY_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check proxy_config required fields
|
||||||
|
CONFIG_PROXY_REQUIRED=$(jq -r '."$defs".proxy_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_PROXY_REQUIRED=$(jq -r '."$defs".proxyConfig.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_PROXY_REQUIRED" != "$HELM_PROXY_REQUIRED" ]; then
|
||||||
|
echo "❌ Proxy config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_PROXY_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_PROXY_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Proxy config required fields match: [$CONFIG_PROXY_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check mcp_client_config required fields
|
||||||
|
# Note: Config uses snake_case (connection_type), Helm uses camelCase (connectionType)
|
||||||
|
CONFIG_MCP_REQUIRED=$(jq -r '."$defs".mcp_client_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_MCP_REQUIRED=$(jq -r '."$defs".mcpClientConfig.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Normalize config snake_case to camelCase for comparison
|
||||||
|
CONFIG_MCP_NORM=$(echo "$CONFIG_MCP_REQUIRED" | tr ',' '\n' | sed 's/connection_type/connectionType/' | sort | tr '\n' ',' | sed 's/,$//')
|
||||||
|
|
||||||
|
if [ "$CONFIG_MCP_NORM" != "$HELM_MCP_REQUIRED" ]; then
|
||||||
|
echo "❌ MCP client config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_MCP_REQUIRED] (normalized: [$CONFIG_MCP_NORM])"
|
||||||
|
echo " Helm: [$HELM_MCP_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ MCP client config required fields match: [$HELM_MCP_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check provider $def required fields
|
||||||
|
CONFIG_PROVIDER_REQUIRED=$(jq -r '."$defs".provider.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_PROVIDER_REQUIRED=$(jq -r '."$defs".provider.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_PROVIDER_REQUIRED" != "$HELM_PROVIDER_REQUIRED" ]; then
|
||||||
|
echo "❌ Provider def required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_PROVIDER_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_PROVIDER_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Provider def required fields match: [$CONFIG_PROVIDER_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check routing_rule required fields
|
||||||
|
CONFIG_ROUTING_REQUIRED=$(jq -r '."$defs".routing_rule.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_ROUTING_REQUIRED=$(jq -r '.properties.bifrost.properties.governance.properties.routingRules.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_ROUTING_REQUIRED" != "$HELM_ROUTING_REQUIRED" ]; then
|
||||||
|
echo "❌ Routing rule required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_ROUTING_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_ROUTING_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Routing rule required fields match: [$CONFIG_ROUTING_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Checking required fields in guardrails..."
|
||||||
|
|
||||||
|
# Check guardrail_rules required fields
|
||||||
|
CONFIG_GUARDRAIL_RULE_REQUIRED=$(jq -r '.properties.guardrails_config.properties.guardrail_rules.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
# Also check in $defs
|
||||||
|
if [ -z "$CONFIG_GUARDRAIL_RULE_REQUIRED" ]; then
|
||||||
|
CONFIG_GUARDRAIL_RULE_REQUIRED=$(jq -r '."$defs".guardrails_config.properties.guardrail_rules.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
fi
|
||||||
|
HELM_GUARDRAIL_RULE_REQUIRED=$(jq -r '.properties.bifrost.properties.guardrails.properties.rules.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_GUARDRAIL_RULE_REQUIRED" != "$HELM_GUARDRAIL_RULE_REQUIRED" ]; then
|
||||||
|
echo "❌ Guardrail rules required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_GUARDRAIL_RULE_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_GUARDRAIL_RULE_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Guardrail rules required fields match: [$CONFIG_GUARDRAIL_RULE_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check guardrail_providers required fields
|
||||||
|
CONFIG_GUARDRAIL_PROV_REQUIRED=$(jq -r '.properties.guardrails_config.properties.guardrail_providers.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
if [ -z "$CONFIG_GUARDRAIL_PROV_REQUIRED" ]; then
|
||||||
|
CONFIG_GUARDRAIL_PROV_REQUIRED=$(jq -r '."$defs".guardrails_config.properties.guardrail_providers.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
fi
|
||||||
|
HELM_GUARDRAIL_PROV_REQUIRED=$(jq -r '.properties.bifrost.properties.guardrails.properties.providers.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_GUARDRAIL_PROV_REQUIRED" != "$HELM_GUARDRAIL_PROV_REQUIRED" ]; then
|
||||||
|
echo "❌ Guardrail providers required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_GUARDRAIL_PROV_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_GUARDRAIL_PROV_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Guardrail providers required fields match: [$CONFIG_GUARDRAIL_PROV_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Checking required fields in cluster config..."
|
||||||
|
|
||||||
|
# Check cluster gossip required fields (port, config)
|
||||||
|
CONFIG_GOSSIP_TOP_REQUIRED=$(jq -r '."$defs".cluster_config.properties.gossip.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_GOSSIP_TOP_REQUIRED=$(jq -r '.properties.bifrost.properties.cluster.properties.gossip.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_GOSSIP_TOP_REQUIRED" != "$HELM_GOSSIP_TOP_REQUIRED" ]; then
|
||||||
|
echo "❌ Cluster gossip required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_GOSSIP_TOP_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_GOSSIP_TOP_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Cluster gossip required fields match: [$CONFIG_GOSSIP_TOP_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check cluster gossip config required fields (timeout_seconds, success_threshold, failure_threshold)
|
||||||
|
CONFIG_GOSSIP_REQUIRED=$(jq -r '."$defs".cluster_config.properties.gossip.properties.config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_GOSSIP_REQUIRED=$(jq -r '.properties.bifrost.properties.cluster.properties.gossip.properties.config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Normalize field names (config uses snake_case, helm uses camelCase)
|
||||||
|
CONFIG_GOSSIP_NORM=$(echo "$CONFIG_GOSSIP_REQUIRED" | tr ',' '\n' | sed 's/failure_threshold/failureThreshold/;s/success_threshold/successThreshold/;s/timeout_seconds/timeoutSeconds/' | sort | tr '\n' ',' | sed 's/,$//')
|
||||||
|
|
||||||
|
if [ "$CONFIG_GOSSIP_NORM" != "$HELM_GOSSIP_REQUIRED" ]; then
|
||||||
|
echo "❌ Cluster gossip config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_GOSSIP_REQUIRED] (normalized: [$CONFIG_GOSSIP_NORM])"
|
||||||
|
echo " Helm: [$HELM_GOSSIP_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Cluster gossip config required fields match: [$HELM_GOSSIP_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Checking required fields in virtual_key_provider_config..."
|
||||||
|
|
||||||
|
# Check virtual_key_provider_config required fields
|
||||||
|
CONFIG_VKPC_REQUIRED=$(jq -r '."$defs".virtual_key_provider_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_VKPC_REQUIRED=$(jq -r '."$defs".virtualKeyProviderConfig.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_VKPC_REQUIRED" != "$HELM_VKPC_REQUIRED" ]; then
|
||||||
|
echo "❌ Virtual key provider config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_VKPC_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_VKPC_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Virtual key provider config required fields match: [$CONFIG_VKPC_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check virtual_key_provider_config keys items required fields (key_id, name, value)
|
||||||
|
CONFIG_VKPC_KEY_REQUIRED=$(jq -r '."$defs".virtual_key_provider_config.properties.keys.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_VKPC_KEY_REQUIRED=$(jq -r '."$defs".virtualKeyProviderConfig.properties.keys.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_VKPC_KEY_REQUIRED" != "$HELM_VKPC_KEY_REQUIRED" ]; then
|
||||||
|
echo "❌ VK provider config key items required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_VKPC_KEY_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_VKPC_KEY_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ VK provider config key items required fields match: [$CONFIG_VKPC_KEY_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check VK provider config key azure_key_config required fields
|
||||||
|
CONFIG_VKPC_AZURE_REQUIRED=$(jq -r '."$defs".virtual_key_provider_config.properties.keys.items.properties.azure_key_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_VKPC_AZURE_REQUIRED=$(jq -r '."$defs".virtualKeyProviderConfig.properties.keys.items.properties.azure_key_config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_VKPC_AZURE_REQUIRED" != "$HELM_VKPC_AZURE_REQUIRED" ]; then
|
||||||
|
echo "❌ VK provider config key azure_key_config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_VKPC_AZURE_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_VKPC_AZURE_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ VK provider config key azure_key_config required fields match: [$CONFIG_VKPC_AZURE_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check VK provider config key vertex_key_config required fields
|
||||||
|
CONFIG_VKPC_VERTEX_REQUIRED=$(jq -r '."$defs".virtual_key_provider_config.properties.keys.items.properties.vertex_key_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_VKPC_VERTEX_REQUIRED=$(jq -r '."$defs".virtualKeyProviderConfig.properties.keys.items.properties.vertex_key_config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_VKPC_VERTEX_REQUIRED" != "$HELM_VKPC_VERTEX_REQUIRED" ]; then
|
||||||
|
echo "❌ VK provider config key vertex_key_config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_VKPC_VERTEX_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_VKPC_VERTEX_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ VK provider config key vertex_key_config required fields match: [$CONFIG_VKPC_VERTEX_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check VK provider config key vllm_key_config required fields
|
||||||
|
CONFIG_VKPC_VLLM_REQUIRED=$(jq -r '."$defs".virtual_key_provider_config.properties.keys.items.properties.vllm_key_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_VKPC_VLLM_REQUIRED=$(jq -r '."$defs".virtualKeyProviderConfig.properties.keys.items.properties.vllm_key_config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_VKPC_VLLM_REQUIRED" != "$HELM_VKPC_VLLM_REQUIRED" ]; then
|
||||||
|
echo "❌ VK provider config key vllm_key_config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_VKPC_VLLM_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_VKPC_VLLM_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ VK provider config key vllm_key_config required fields match: [$CONFIG_VKPC_VLLM_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Checking required fields in virtual key MCP config..."
|
||||||
|
|
||||||
|
# Check virtual_key_mcp_config required fields
|
||||||
|
CONFIG_VK_MCP_REQUIRED=$(jq -r '."$defs".virtual_key_mcp_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_VK_MCP_REQUIRED=$(jq -r '.properties.bifrost.properties.governance.properties.virtualKeys.items.properties.mcp_configs.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_VK_MCP_REQUIRED" != "$HELM_VK_MCP_REQUIRED" ]; then
|
||||||
|
echo "❌ Virtual key MCP config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_VK_MCP_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_VK_MCP_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Virtual key MCP config required fields match: [$CONFIG_VK_MCP_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Checking required fields in MCP sub-configs..."
|
||||||
|
|
||||||
|
# Check MCP stdio_config required fields
|
||||||
|
CONFIG_MCP_STDIO_REQUIRED=$(jq -r '."$defs".mcp_client_config.properties.stdio_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_MCP_STDIO_REQUIRED=$(jq -r '."$defs".mcpClientConfig.properties.stdioConfig.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_MCP_STDIO_REQUIRED" != "$HELM_MCP_STDIO_REQUIRED" ]; then
|
||||||
|
echo "❌ MCP stdio config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_MCP_STDIO_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_MCP_STDIO_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ MCP stdio config required fields match: [$CONFIG_MCP_STDIO_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# MCP websocket_config and http_config were removed from config.schema.json
|
||||||
|
# because the corresponding Go fields don't exist (MCP rendering uses
|
||||||
|
# connection_type + connection_string directly, not sub-object configs).
|
||||||
|
# Helm still declares them for user convenience — not a schema sync concern.
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Checking required fields in SAML/SCIM config..."
|
||||||
|
|
||||||
|
# Check okta_config required fields
|
||||||
|
CONFIG_OKTA_REQUIRED=$(jq -r '."$defs".okta_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_OKTA_REQUIRED=$(jq -r '.properties.bifrost.properties.scim.allOf[0].then.properties.config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_OKTA_REQUIRED" != "$HELM_OKTA_REQUIRED" ]; then
|
||||||
|
echo "❌ Okta config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_OKTA_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_OKTA_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Okta config required fields match: [$CONFIG_OKTA_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check entra_config required fields
|
||||||
|
CONFIG_ENTRA_REQUIRED=$(jq -r '."$defs".entra_config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_ENTRA_REQUIRED=$(jq -r '.properties.bifrost.properties.scim.allOf[1].then.properties.config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_ENTRA_REQUIRED" != "$HELM_ENTRA_REQUIRED" ]; then
|
||||||
|
echo "❌ Entra config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_ENTRA_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_ENTRA_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Entra config required fields match: [$CONFIG_ENTRA_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Checking required fields in plugin configs..."
|
||||||
|
|
||||||
|
# Check semantic cache plugin required fields (dimension)
|
||||||
|
# Config uses an allOf pattern on plugins array items; Helm uses conditional on semanticCache.enabled
|
||||||
|
CONFIG_SEMCACHE_REQUIRED=$(jq -r '.properties.plugins.items.allOf[] | select(.if.properties.name.const == "semantic_cache") | .then.properties.config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_SEMCACHE_REQUIRED=$(jq -r '.properties.bifrost.properties.plugins.properties.semanticCache.then.properties.config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_SEMCACHE_REQUIRED" != "$HELM_SEMCACHE_REQUIRED" ]; then
|
||||||
|
echo "❌ Semantic cache plugin config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_SEMCACHE_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_SEMCACHE_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Semantic cache plugin config required fields match: [$CONFIG_SEMCACHE_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check OTEL plugin required fields (collector_url, trace_type, protocol)
|
||||||
|
CONFIG_OTEL_REQUIRED=$(jq -r '.properties.plugins.items.allOf[] | select(.if.properties.name.const == "otel") | .then.properties.config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_OTEL_REQUIRED=$(jq -r '.properties.bifrost.properties.plugins.properties.otel.then.properties.config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_OTEL_REQUIRED" != "$HELM_OTEL_REQUIRED" ]; then
|
||||||
|
echo "❌ OTEL plugin config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_OTEL_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_OTEL_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ OTEL plugin config required fields match: [$CONFIG_OTEL_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check telemetry push_gateway required fields
|
||||||
|
CONFIG_PUSHGW_REQUIRED=$(jq -r '.properties.plugins.items.allOf[] | select(.if.properties.name.const == "telemetry") | .then.properties.config.properties.push_gateway.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_PUSHGW_REQUIRED=$(jq -r '.properties.bifrost.properties.plugins.properties.telemetry.properties.config.properties.push_gateway.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_PUSHGW_REQUIRED" != "$HELM_PUSHGW_REQUIRED" ]; then
|
||||||
|
echo "❌ Telemetry push_gateway required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_PUSHGW_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_PUSHGW_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Telemetry push_gateway required fields match: [$CONFIG_PUSHGW_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check telemetry push_gateway basic_auth required fields
|
||||||
|
CONFIG_PUSHGW_AUTH_REQUIRED=$(jq -r '.properties.plugins.items.allOf[] | select(.if.properties.name.const == "telemetry") | .then.properties.config.properties.push_gateway.properties.basic_auth.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_PUSHGW_AUTH_REQUIRED=$(jq -r '.properties.bifrost.properties.plugins.properties.telemetry.properties.config.properties.push_gateway.properties.basic_auth.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_PUSHGW_AUTH_REQUIRED" != "$HELM_PUSHGW_AUTH_REQUIRED" ]; then
|
||||||
|
echo "❌ Telemetry push_gateway basic_auth required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_PUSHGW_AUTH_REQUIRED]"
|
||||||
|
echo " Helm: [$HELM_PUSHGW_AUTH_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Telemetry push_gateway basic_auth required fields match: [$CONFIG_PUSHGW_AUTH_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check plugin array items required fields (enabled, name)
|
||||||
|
# Config defines plugins as an array; Helm splits into named plugins + a "custom" array
|
||||||
|
CONFIG_PLUGIN_ITEMS_REQUIRED=$(jq -r '.properties.plugins.items.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_PLUGIN_ITEMS_REQUIRED=$(jq -r '.properties.bifrost.properties.plugins.properties.custom.items.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_PLUGIN_ITEMS_REQUIRED" != "$HELM_PLUGIN_ITEMS_REQUIRED" ]; then
|
||||||
|
echo "❌ Plugin items required fields mismatch:"
|
||||||
|
echo " Config (plugins.items): [$CONFIG_PLUGIN_ITEMS_REQUIRED]"
|
||||||
|
echo " Helm (plugins.custom.items): [$HELM_PLUGIN_ITEMS_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Plugin items required fields match: [$CONFIG_PLUGIN_ITEMS_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check plugin item properties completeness (all config properties must exist in helm custom items)
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Checking plugin item property completeness..."
|
||||||
|
|
||||||
|
CONFIG_PLUGIN_PROPS=$(jq -r '.properties.plugins.items.properties | keys | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_CUSTOM_PLUGIN_PROPS=$(jq -r '.properties.bifrost.properties.plugins.properties.custom.items.properties | keys | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Check each config property exists in helm custom items
|
||||||
|
for prop in $(echo "$CONFIG_PLUGIN_PROPS" | tr ',' '\n'); do
|
||||||
|
if ! echo "$HELM_CUSTOM_PLUGIN_PROPS" | tr ',' '\n' | grep -qx "$prop"; then
|
||||||
|
echo "❌ Plugin property '$prop' exists in config.schema.json but missing from helm custom plugin items"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Plugin property '$prop' present in both schemas"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Verify placement enum values match
|
||||||
|
CONFIG_PLACEMENT_ENUM=$(jq -r '.properties.plugins.items.properties.placement.enum // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_PLACEMENT_ENUM=$(jq -r '.properties.bifrost.properties.plugins.properties.custom.items.properties.placement.enum // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_PLACEMENT_ENUM" != "$HELM_PLACEMENT_ENUM" ]; then
|
||||||
|
echo "❌ Plugin placement enum mismatch:"
|
||||||
|
echo " Config: [$CONFIG_PLACEMENT_ENUM]"
|
||||||
|
echo " Helm: [$HELM_PLACEMENT_ENUM]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Plugin placement enum values match: [$CONFIG_PLACEMENT_ENUM]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check maxim plugin config required fields (api_key)
|
||||||
|
# Note: Helm allows either config.api_key OR secretRef.name via anyOf
|
||||||
|
CONFIG_MAXIM_REQUIRED=$(jq -r '.properties.plugins.items.allOf[] | select(.if.properties.name.const == "maxim") | .then.properties.config.required // [] | sort | join(",")' "$CONFIG_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
HELM_MAXIM_REQUIRED=$(jq -r '.properties.bifrost.properties.plugins.properties.maxim.then.anyOf[0].properties.config.required // [] | sort | join(",")' "$HELM_SCHEMA" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CONFIG_MAXIM_REQUIRED" != "$HELM_MAXIM_REQUIRED" ]; then
|
||||||
|
echo "❌ Maxim plugin config required fields mismatch:"
|
||||||
|
echo " Config: [$CONFIG_MAXIM_REQUIRED]"
|
||||||
|
echo " Helm (anyOf[0]): [$HELM_MAXIM_REQUIRED]"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "✅ Maxim plugin config required fields match: [$CONFIG_MAXIM_REQUIRED]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Checking property existence for Gap 1-8 fields..."
|
||||||
|
|
||||||
|
# Helper function to check a property exists in a schema
|
||||||
|
check_property_exists() {
|
||||||
|
local label=$1
|
||||||
|
local jq_path=$2
|
||||||
|
local schema_file=$3
|
||||||
|
if ! jq -e "$jq_path" "$schema_file" > /dev/null 2>&1; then
|
||||||
|
echo " ❌ Missing: $label"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo " ✅ Present: $label"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gap 1+2: Client properties in Helm schema
|
||||||
|
echo ""
|
||||||
|
echo " Checking client properties (Gap 1+2)..."
|
||||||
|
for prop in asyncJobResultTTL requiredHeaders loggingHeaders allowedHeaders mcpAgentDepth mcpToolExecutionTimeout mcpCodeModeBindingLevel mcpToolSyncInterval hideDeletedVirtualKeysInFilters; do
|
||||||
|
check_property_exists "client.$prop" ".properties.bifrost.properties.client.properties.${prop}" "$HELM_SCHEMA"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Gap 3: OTel plugin config properties
|
||||||
|
echo ""
|
||||||
|
echo " Checking OTel plugin properties (Gap 3)..."
|
||||||
|
for prop in headers tls_ca_cert insecure; do
|
||||||
|
check_property_exists "otel.config.$prop" ".properties.bifrost.properties.plugins.properties.otel.properties.config.properties.${prop}" "$HELM_SCHEMA"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Gap 4: Governance plugin config properties
|
||||||
|
echo ""
|
||||||
|
echo " Checking governance plugin properties (Gap 4)..."
|
||||||
|
for prop in required_headers is_enterprise; do
|
||||||
|
check_property_exists "governance.plugin.config.$prop" ".properties.bifrost.properties.plugins.properties.governance.properties.config.properties.${prop}" "$HELM_SCHEMA"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Gap 5: Governance top-level properties
|
||||||
|
echo ""
|
||||||
|
echo " Checking governance top-level properties (Gap 5)..."
|
||||||
|
for prop in modelConfigs providers; do
|
||||||
|
check_property_exists "governance.$prop" ".properties.bifrost.properties.governance.properties.${prop}" "$HELM_SCHEMA"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Gap 6: MCP properties
|
||||||
|
echo ""
|
||||||
|
echo " Checking MCP properties (Gap 6)..."
|
||||||
|
check_property_exists "mcp.toolSyncInterval" ".properties.bifrost.properties.mcp.properties.toolSyncInterval" "$HELM_SCHEMA"
|
||||||
|
check_property_exists "mcp.toolManagerConfig.codeModeBindingLevel" '.properties.bifrost.properties.mcp.properties.toolManagerConfig.properties.codeModeBindingLevel' "$HELM_SCHEMA"
|
||||||
|
for prop in clientId isCodeModeClient toolSyncInterval isPingAvailable; do
|
||||||
|
check_property_exists "mcpClientConfig.$prop" '.["$defs"].mcpClientConfig.properties.'"${prop}" "$HELM_SCHEMA"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Gap 7: Cluster properties
|
||||||
|
echo ""
|
||||||
|
echo " Checking cluster properties (Gap 7)..."
|
||||||
|
check_property_exists "cluster.region" ".properties.bifrost.properties.cluster.properties.region" "$HELM_SCHEMA"
|
||||||
|
|
||||||
|
# Gap 8: Miscellaneous properties
|
||||||
|
echo ""
|
||||||
|
echo " Checking miscellaneous properties (Gap 8)..."
|
||||||
|
check_property_exists "telemetry.custom_labels" ".properties.bifrost.properties.plugins.properties.telemetry.properties.config.properties.custom_labels" "$HELM_SCHEMA"
|
||||||
|
check_property_exists "semanticCache.default_cache_key" ".properties.bifrost.properties.plugins.properties.semanticCache.properties.config.properties.default_cache_key" "$HELM_SCHEMA"
|
||||||
|
|
||||||
|
# Also verify these exist in config.schema.json
|
||||||
|
echo ""
|
||||||
|
echo " Checking config.schema.json has is_ping_available + tool_pricing..."
|
||||||
|
check_property_exists "mcp_client_config.is_ping_available" '."$defs".mcp_client_config.properties.is_ping_available' "$CONFIG_SCHEMA"
|
||||||
|
check_property_exists "mcp_client_config.tool_pricing" '."$defs".mcp_client_config.properties.tool_pricing' "$CONFIG_SCHEMA"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [ $ERRORS -gt 0 ]; then
|
||||||
|
echo "❌ Schema validation failed with $ERRORS error(s)"
|
||||||
|
echo ""
|
||||||
|
echo "To fix these errors, update helm-charts/bifrost/values.schema.json to match"
|
||||||
|
echo "the required fields in transports/config.schema.json"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ All schema validations passed!"
|
||||||
|
exit 0
|
||||||
437
.github/workflows/scripts/validate-helm-templates.sh
vendored
Executable file
437
.github/workflows/scripts/validate-helm-templates.sh
vendored
Executable file
@@ -0,0 +1,437 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Helm template validation script for Bifrost
|
||||||
|
# Validates all storage and vector store combinations render correctly
|
||||||
|
|
||||||
|
echo "🔍 Validating Helm Chart Templates..."
|
||||||
|
echo "======================================"
|
||||||
|
|
||||||
|
# Color codes for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Track test results
|
||||||
|
TESTS_PASSED=0
|
||||||
|
TESTS_FAILED=0
|
||||||
|
|
||||||
|
# Function to report test result
|
||||||
|
report_result() {
|
||||||
|
local test_name=$1
|
||||||
|
local result=$2
|
||||||
|
|
||||||
|
if [ "$result" -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}✅ $test_name${NC}"
|
||||||
|
TESTS_PASSED=$((TESTS_PASSED + 1))
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ $test_name${NC}"
|
||||||
|
TESTS_FAILED=$((TESTS_FAILED + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to test a helm template combination
|
||||||
|
test_template() {
|
||||||
|
local test_name=$1
|
||||||
|
shift
|
||||||
|
local helm_args=("$@")
|
||||||
|
|
||||||
|
if helm template bifrost ./helm-charts/bifrost \
|
||||||
|
--set image.tag=v1.0.0 \
|
||||||
|
"${helm_args[@]}" \
|
||||||
|
> /tmp/helm-template-output.yaml 2>&1; then
|
||||||
|
report_result "$test_name" 0
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
report_result "$test_name" 1
|
||||||
|
echo -e "${YELLOW} Error output:${NC}"
|
||||||
|
head -10 /tmp/helm-template-output.yaml | sed 's/^/ /'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. Storage Combinations (9 tests)
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}📦 1/6 - Testing Storage Combinations (9 tests)...${NC}"
|
||||||
|
echo "---------------------------------------------------"
|
||||||
|
|
||||||
|
# config=no, logs=no
|
||||||
|
test_template "config=no, logs=no" \
|
||||||
|
--set storage.configStore.enabled=false \
|
||||||
|
--set storage.logsStore.enabled=false \
|
||||||
|
--set postgresql.enabled=false
|
||||||
|
|
||||||
|
# config=no, logs=sqlite
|
||||||
|
test_template "config=no, logs=sqlite" \
|
||||||
|
--set storage.configStore.enabled=false \
|
||||||
|
--set storage.logsStore.enabled=true \
|
||||||
|
--set storage.mode=sqlite \
|
||||||
|
--set postgresql.enabled=false
|
||||||
|
|
||||||
|
# config=no, logs=postgres
|
||||||
|
test_template "config=no, logs=postgres" \
|
||||||
|
--set storage.configStore.enabled=false \
|
||||||
|
--set storage.logsStore.enabled=true \
|
||||||
|
--set storage.mode=postgres \
|
||||||
|
--set postgresql.enabled=true \
|
||||||
|
--set postgresql.auth.password=testpass
|
||||||
|
|
||||||
|
# config=sqlite, logs=no
|
||||||
|
test_template "config=sqlite, logs=no" \
|
||||||
|
--set storage.configStore.enabled=true \
|
||||||
|
--set storage.logsStore.enabled=false \
|
||||||
|
--set storage.mode=sqlite \
|
||||||
|
--set postgresql.enabled=false
|
||||||
|
|
||||||
|
# config=sqlite, logs=sqlite
|
||||||
|
test_template "config=sqlite, logs=sqlite" \
|
||||||
|
--set storage.configStore.enabled=true \
|
||||||
|
--set storage.logsStore.enabled=true \
|
||||||
|
--set storage.mode=sqlite \
|
||||||
|
--set postgresql.enabled=false
|
||||||
|
|
||||||
|
# config=sqlite, logs=postgres
|
||||||
|
test_template "config=sqlite, logs=postgres" \
|
||||||
|
--set storage.configStore.enabled=true \
|
||||||
|
--set storage.logsStore.enabled=true \
|
||||||
|
--set storage.mode=postgres \
|
||||||
|
--set postgresql.enabled=true \
|
||||||
|
--set postgresql.auth.password=testpass
|
||||||
|
|
||||||
|
# config=postgres, logs=no
|
||||||
|
test_template "config=postgres, logs=no" \
|
||||||
|
--set storage.configStore.enabled=true \
|
||||||
|
--set storage.logsStore.enabled=false \
|
||||||
|
--set storage.mode=postgres \
|
||||||
|
--set postgresql.enabled=true \
|
||||||
|
--set postgresql.auth.password=testpass
|
||||||
|
|
||||||
|
# config=postgres, logs=sqlite
|
||||||
|
test_template "config=postgres, logs=sqlite" \
|
||||||
|
--set storage.configStore.enabled=true \
|
||||||
|
--set storage.logsStore.enabled=true \
|
||||||
|
--set storage.mode=postgres \
|
||||||
|
--set postgresql.enabled=true \
|
||||||
|
--set postgresql.auth.password=testpass
|
||||||
|
|
||||||
|
# config=postgres, logs=postgres
|
||||||
|
test_template "config=postgres, logs=postgres" \
|
||||||
|
--set storage.configStore.enabled=true \
|
||||||
|
--set storage.logsStore.enabled=true \
|
||||||
|
--set storage.mode=postgres \
|
||||||
|
--set postgresql.enabled=true \
|
||||||
|
--set postgresql.auth.password=testpass
|
||||||
|
|
||||||
|
# 2. Vector Store Combinations (6 tests)
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}🗄️ 2/6 - Testing Vector Store Combinations (6 tests)...${NC}"
|
||||||
|
echo "--------------------------------------------------------"
|
||||||
|
|
||||||
|
# Weaviate
|
||||||
|
test_template "vectorStore=weaviate" \
|
||||||
|
--set vectorStore.enabled=true \
|
||||||
|
--set vectorStore.type=weaviate \
|
||||||
|
--set vectorStore.weaviate.enabled=true
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
test_template "vectorStore=redis" \
|
||||||
|
--set vectorStore.enabled=true \
|
||||||
|
--set vectorStore.type=redis \
|
||||||
|
--set vectorStore.redis.enabled=true
|
||||||
|
|
||||||
|
# Qdrant
|
||||||
|
test_template "vectorStore=qdrant" \
|
||||||
|
--set vectorStore.enabled=true \
|
||||||
|
--set vectorStore.type=qdrant \
|
||||||
|
--set vectorStore.qdrant.enabled=true
|
||||||
|
|
||||||
|
# postgres + weaviate
|
||||||
|
test_template "postgres + weaviate" \
|
||||||
|
--set storage.mode=postgres \
|
||||||
|
--set postgresql.enabled=true \
|
||||||
|
--set postgresql.auth.password=testpass \
|
||||||
|
--set vectorStore.enabled=true \
|
||||||
|
--set vectorStore.type=weaviate \
|
||||||
|
--set vectorStore.weaviate.enabled=true
|
||||||
|
|
||||||
|
# postgres + qdrant
|
||||||
|
test_template "postgres + qdrant" \
|
||||||
|
--set storage.mode=postgres \
|
||||||
|
--set postgresql.enabled=true \
|
||||||
|
--set postgresql.auth.password=testpass \
|
||||||
|
--set vectorStore.enabled=true \
|
||||||
|
--set vectorStore.type=qdrant \
|
||||||
|
--set vectorStore.qdrant.enabled=true
|
||||||
|
|
||||||
|
# sqlite + qdrant
|
||||||
|
test_template "sqlite + qdrant" \
|
||||||
|
--set storage.mode=sqlite \
|
||||||
|
--set postgresql.enabled=false \
|
||||||
|
--set vectorStore.enabled=true \
|
||||||
|
--set vectorStore.type=qdrant \
|
||||||
|
--set vectorStore.qdrant.enabled=true
|
||||||
|
|
||||||
|
# 3. Special Configurations (7 tests)
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}⚙️ 3/6 - Testing Special Configurations (7 tests)...${NC}"
|
||||||
|
echo "-----------------------------------------------------"
|
||||||
|
|
||||||
|
# semantic cache: direct mode (dimension: 1, no provider/keys)
|
||||||
|
test_template "semanticCache: direct mode (dimension: 1)" \
|
||||||
|
--set bifrost.plugins.semanticCache.enabled=true \
|
||||||
|
--set bifrost.plugins.semanticCache.config.dimension=1 \
|
||||||
|
--set bifrost.plugins.semanticCache.config.ttl=30m \
|
||||||
|
--set vectorStore.enabled=true \
|
||||||
|
--set vectorStore.type=redis \
|
||||||
|
--set vectorStore.redis.enabled=true
|
||||||
|
|
||||||
|
# semantic cache: semantic mode (dimension > 1, requires provider/keys)
|
||||||
|
test_template "semanticCache: semantic mode (dimension: 1536)" \
|
||||||
|
--set bifrost.plugins.semanticCache.enabled=true \
|
||||||
|
--set bifrost.plugins.semanticCache.config.dimension=1536 \
|
||||||
|
--set bifrost.plugins.semanticCache.config.provider=openai \
|
||||||
|
--set 'bifrost.plugins.semanticCache.config.keys[0]=sk-test' \
|
||||||
|
--set vectorStore.enabled=true \
|
||||||
|
--set vectorStore.type=redis \
|
||||||
|
--set vectorStore.redis.enabled=true
|
||||||
|
|
||||||
|
# semantic cache: direct mode with redis + postgres
|
||||||
|
test_template "semanticCache: direct mode + postgres" \
|
||||||
|
--set bifrost.plugins.semanticCache.enabled=true \
|
||||||
|
--set bifrost.plugins.semanticCache.config.dimension=1 \
|
||||||
|
--set storage.mode=postgres \
|
||||||
|
--set postgresql.enabled=true \
|
||||||
|
--set postgresql.auth.password=testpass \
|
||||||
|
--set vectorStore.enabled=true \
|
||||||
|
--set vectorStore.type=redis \
|
||||||
|
--set vectorStore.redis.enabled=true
|
||||||
|
|
||||||
|
# sqlite + persistence + autoscaling (StatefulSet HPA)
|
||||||
|
test_template "sqlite + persistence + autoscaling (StatefulSet)" \
|
||||||
|
--set storage.mode=sqlite \
|
||||||
|
--set storage.persistence.enabled=true \
|
||||||
|
--set postgresql.enabled=false \
|
||||||
|
--set autoscaling.enabled=true \
|
||||||
|
--set autoscaling.minReplicas=2 \
|
||||||
|
--set autoscaling.maxReplicas=5
|
||||||
|
|
||||||
|
# postgres + autoscaling (Deployment HPA)
|
||||||
|
test_template "postgres + autoscaling (Deployment)" \
|
||||||
|
--set storage.mode=postgres \
|
||||||
|
--set postgresql.enabled=true \
|
||||||
|
--set postgresql.auth.password=testpass \
|
||||||
|
--set autoscaling.enabled=true \
|
||||||
|
--set autoscaling.minReplicas=2 \
|
||||||
|
--set autoscaling.maxReplicas=5
|
||||||
|
|
||||||
|
# ingress enabled
|
||||||
|
test_template "ingress enabled" \
|
||||||
|
--set ingress.enabled=true \
|
||||||
|
--set ingress.className=nginx \
|
||||||
|
--set 'ingress.hosts[0].host=bifrost.example.com' \
|
||||||
|
--set 'ingress.hosts[0].paths[0].path=/' \
|
||||||
|
--set 'ingress.hosts[0].paths[0].pathType=Prefix'
|
||||||
|
|
||||||
|
# full production-like config
|
||||||
|
test_template "production-like config" \
|
||||||
|
--set storage.mode=postgres \
|
||||||
|
--set postgresql.enabled=true \
|
||||||
|
--set postgresql.auth.password=testpass \
|
||||||
|
--set vectorStore.enabled=true \
|
||||||
|
--set vectorStore.type=qdrant \
|
||||||
|
--set vectorStore.qdrant.enabled=true \
|
||||||
|
--set autoscaling.enabled=true \
|
||||||
|
--set ingress.enabled=true \
|
||||||
|
--set ingress.className=nginx \
|
||||||
|
--set 'ingress.hosts[0].host=bifrost.example.com' \
|
||||||
|
--set 'ingress.hosts[0].paths[0].path=/' \
|
||||||
|
--set 'ingress.hosts[0].paths[0].pathType=Prefix'
|
||||||
|
|
||||||
|
# 4. New Property Rendering (Gap 1-8 tests)
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}🆕 4/6 - Testing New Property Rendering (Gap 1-8)...${NC}"
|
||||||
|
echo "-----------------------------------------------------"
|
||||||
|
|
||||||
|
# Gap 1+2: Client new properties
|
||||||
|
test_template "client: new properties (Gap 1+2)" \
|
||||||
|
--set bifrost.client.asyncJobResultTTL=300 \
|
||||||
|
--set 'bifrost.client.requiredHeaders[0]=X-Request-ID' \
|
||||||
|
--set 'bifrost.client.loggingHeaders[0]=X-Trace-ID' \
|
||||||
|
--set 'bifrost.client.allowedHeaders[0]=Authorization' \
|
||||||
|
--set bifrost.client.mcpAgentDepth=5 \
|
||||||
|
--set bifrost.client.mcpToolExecutionTimeout=30 \
|
||||||
|
--set bifrost.client.mcpCodeModeBindingLevel=server \
|
||||||
|
--set bifrost.client.mcpToolSyncInterval=60 \
|
||||||
|
--set bifrost.client.hideDeletedVirtualKeysInFilters=true
|
||||||
|
|
||||||
|
# Gap 3: OTel plugin with new fields
|
||||||
|
test_template "otel: headers + tls_ca_cert + insecure (Gap 3)" \
|
||||||
|
--set bifrost.plugins.otel.enabled=true \
|
||||||
|
--set bifrost.plugins.otel.config.collector_url=otel:4317 \
|
||||||
|
--set bifrost.plugins.otel.config.trace_type=genai_extension \
|
||||||
|
--set bifrost.plugins.otel.config.protocol=grpc \
|
||||||
|
--set 'bifrost.plugins.otel.config.headers.Authorization=Bearer token' \
|
||||||
|
--set bifrost.plugins.otel.config.tls_ca_cert=/certs/ca.pem \
|
||||||
|
--set bifrost.plugins.otel.config.insecure=true
|
||||||
|
|
||||||
|
# Gap 4: Governance plugin with new fields
|
||||||
|
test_template "governance: required_headers + is_enterprise (Gap 4)" \
|
||||||
|
--set bifrost.plugins.governance.enabled=true \
|
||||||
|
--set 'bifrost.plugins.governance.config.required_headers[0]=X-Team-ID' \
|
||||||
|
--set bifrost.plugins.governance.config.is_enterprise=true
|
||||||
|
|
||||||
|
# Gap 5: Governance modelConfigs + providers
|
||||||
|
test_template "governance: modelConfigs + providers (Gap 5)" \
|
||||||
|
--set 'bifrost.governance.modelConfigs[0].id=mc-1' \
|
||||||
|
--set 'bifrost.governance.modelConfigs[0].model_name=gpt-4o' \
|
||||||
|
--set 'bifrost.governance.providers[0].name=openai'
|
||||||
|
|
||||||
|
# Gap 6: MCP new fields
|
||||||
|
test_template "mcp: toolSyncInterval + codeModeBindingLevel (Gap 6)" \
|
||||||
|
--set bifrost.mcp.enabled=true \
|
||||||
|
--set bifrost.mcp.toolSyncInterval=10m \
|
||||||
|
--set bifrost.mcp.toolManagerConfig.codeModeBindingLevel=server \
|
||||||
|
--set 'bifrost.mcp.clientConfigs[0].name=test' \
|
||||||
|
--set 'bifrost.mcp.clientConfigs[0].connectionType=http' \
|
||||||
|
--set 'bifrost.mcp.clientConfigs[0].httpConfig.url=http://localhost:3000' \
|
||||||
|
--set 'bifrost.mcp.clientConfigs[0].clientId=client-1' \
|
||||||
|
--set 'bifrost.mcp.clientConfigs[0].isCodeModeClient=true' \
|
||||||
|
--set 'bifrost.mcp.clientConfigs[0].toolSyncInterval=5m'
|
||||||
|
|
||||||
|
# Gap 7: Cluster with region
|
||||||
|
test_template "cluster: region (Gap 7)" \
|
||||||
|
--set bifrost.cluster.enabled=true \
|
||||||
|
--set 'bifrost.cluster.peers[0]=peer-0:7946' \
|
||||||
|
--set bifrost.cluster.gossip.port=7946 \
|
||||||
|
--set bifrost.cluster.gossip.config.timeoutSeconds=10 \
|
||||||
|
--set bifrost.cluster.gossip.config.successThreshold=3 \
|
||||||
|
--set bifrost.cluster.gossip.config.failureThreshold=3 \
|
||||||
|
--set bifrost.cluster.region=us-east-1
|
||||||
|
|
||||||
|
# Gap 8: Combined production-like with all new fields
|
||||||
|
test_template "combined: all new Gap 1-8 fields" \
|
||||||
|
--set bifrost.client.asyncJobResultTTL=300 \
|
||||||
|
--set bifrost.client.mcpAgentDepth=5 \
|
||||||
|
--set bifrost.client.hideDeletedVirtualKeysInFilters=true \
|
||||||
|
--set bifrost.plugins.otel.enabled=true \
|
||||||
|
--set bifrost.plugins.otel.config.collector_url=otel:4317 \
|
||||||
|
--set bifrost.plugins.otel.config.trace_type=genai_extension \
|
||||||
|
--set bifrost.plugins.otel.config.protocol=grpc \
|
||||||
|
--set bifrost.plugins.otel.config.insecure=true \
|
||||||
|
--set bifrost.plugins.governance.enabled=true \
|
||||||
|
--set bifrost.plugins.governance.config.is_enterprise=true \
|
||||||
|
--set bifrost.cluster.enabled=true \
|
||||||
|
--set 'bifrost.cluster.peers[0]=peer-0:7946' \
|
||||||
|
--set bifrost.cluster.gossip.port=7946 \
|
||||||
|
--set bifrost.cluster.gossip.config.timeoutSeconds=10 \
|
||||||
|
--set bifrost.cluster.gossip.config.successThreshold=3 \
|
||||||
|
--set bifrost.cluster.gossip.config.failureThreshold=3 \
|
||||||
|
--set bifrost.cluster.region=us-west-2
|
||||||
|
|
||||||
|
# 5. Plugin Name Validation
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}🔌 5/6 - Validating Plugin Names Match Go Registry...${NC}"
|
||||||
|
echo "------------------------------------------------------"
|
||||||
|
|
||||||
|
# Verify semantic cache plugin renders with correct name ("semantic_cache", not "semantic_cache")
|
||||||
|
# Go registry: plugins/semantic_cache/main.go defines PluginName = "semantic_cache"
|
||||||
|
test_name="semanticCache plugin name matches Go registry (semantic_cache)"
|
||||||
|
if helm template bifrost ./helm-charts/bifrost \
|
||||||
|
--set image.tag=v1.0.0 \
|
||||||
|
--set bifrost.plugins.semanticCache.enabled=true \
|
||||||
|
--set bifrost.plugins.semanticCache.config.dimension=1536 \
|
||||||
|
--set bifrost.plugins.semanticCache.config.provider=openai \
|
||||||
|
--set 'bifrost.plugins.semanticCache.config.keys[0]=sk-test' \
|
||||||
|
--set vectorStore.enabled=true \
|
||||||
|
--set vectorStore.type=redis \
|
||||||
|
--set vectorStore.redis.enabled=true \
|
||||||
|
> /tmp/helm-template-output.yaml 2>&1; then
|
||||||
|
if grep -Eq '"name"[[:space:]]*:[[:space:]]*"semantic_cache"' /tmp/helm-template-output.yaml; then
|
||||||
|
report_result "$test_name" 0
|
||||||
|
else
|
||||||
|
report_result "$test_name" 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
report_result "$test_name" 1
|
||||||
|
echo -e "${YELLOW} Error output:${NC}"
|
||||||
|
head -10 /tmp/helm-template-output.yaml | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. Custom Plugin Placement and Order Rendering
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}🔧 6/6 - Validating Custom Plugin placement and order Rendering...${NC}"
|
||||||
|
echo "-------------------------------------------------------------------"
|
||||||
|
|
||||||
|
# Test custom plugin renders successfully with placement and order
|
||||||
|
test_template "custom plugin with placement and order" \
|
||||||
|
--set 'bifrost.plugins.custom[0].name=my-plugin' \
|
||||||
|
--set 'bifrost.plugins.custom[0].enabled=true' \
|
||||||
|
--set 'bifrost.plugins.custom[0].path=/plugins/my-plugin.so' \
|
||||||
|
--set 'bifrost.plugins.custom[0].placement=pre_builtin' \
|
||||||
|
--set 'bifrost.plugins.custom[0].order=2'
|
||||||
|
|
||||||
|
# Verify placement appears in rendered output
|
||||||
|
test_name="custom plugin rendered JSON contains placement field"
|
||||||
|
if helm template bifrost ./helm-charts/bifrost \
|
||||||
|
--set image.tag=v1.0.0 \
|
||||||
|
--set 'bifrost.plugins.custom[0].name=my-plugin' \
|
||||||
|
--set 'bifrost.plugins.custom[0].enabled=true' \
|
||||||
|
--set 'bifrost.plugins.custom[0].path=/plugins/my-plugin.so' \
|
||||||
|
--set 'bifrost.plugins.custom[0].placement=pre_builtin' \
|
||||||
|
--set 'bifrost.plugins.custom[0].order=2' \
|
||||||
|
> /tmp/helm-template-output.yaml 2>&1; then
|
||||||
|
if grep -Eq '"placement"[[:space:]]*:[[:space:]]*"pre_builtin"' /tmp/helm-template-output.yaml; then
|
||||||
|
report_result "$test_name" 0
|
||||||
|
else
|
||||||
|
report_result "$test_name" 1
|
||||||
|
echo -e "${YELLOW} placement field not found in rendered output${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
report_result "$test_name" 1
|
||||||
|
echo -e "${YELLOW} Error output:${NC}"
|
||||||
|
head -10 /tmp/helm-template-output.yaml | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify order appears in rendered output
|
||||||
|
test_name="custom plugin rendered JSON contains order field"
|
||||||
|
if helm template bifrost ./helm-charts/bifrost \
|
||||||
|
--set image.tag=v1.0.0 \
|
||||||
|
--set 'bifrost.plugins.custom[0].name=my-plugin' \
|
||||||
|
--set 'bifrost.plugins.custom[0].enabled=true' \
|
||||||
|
--set 'bifrost.plugins.custom[0].path=/plugins/my-plugin.so' \
|
||||||
|
--set 'bifrost.plugins.custom[0].placement=pre_builtin' \
|
||||||
|
--set 'bifrost.plugins.custom[0].order=2' \
|
||||||
|
> /tmp/helm-template-output.yaml 2>&1; then
|
||||||
|
if grep -Eq '"order"[[:space:]]*:[[:space:]]*2' /tmp/helm-template-output.yaml; then
|
||||||
|
report_result "$test_name" 0
|
||||||
|
else
|
||||||
|
report_result "$test_name" 1
|
||||||
|
echo -e "${YELLOW} order field not found in rendered output${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
report_result "$test_name" 1
|
||||||
|
echo -e "${YELLOW} Error output:${NC}"
|
||||||
|
head -10 /tmp/helm-template-output.yaml | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm -f /tmp/helm-template-output.yaml
|
||||||
|
|
||||||
|
# Final Summary
|
||||||
|
echo ""
|
||||||
|
echo "======================================"
|
||||||
|
echo "🏁 Helm Template Validation Complete!"
|
||||||
|
echo "======================================"
|
||||||
|
echo -e "${GREEN}Passed: $TESTS_PASSED${NC}"
|
||||||
|
echo -e "${RED}Failed: $TESTS_FAILED${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$TESTS_FAILED" -gt 0 ]; then
|
||||||
|
echo -e "${RED}❌ Some template validations failed. Please review the output above.${NC}"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✅ All template validations passed successfully!${NC}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
63
.github/workflows/scripts/validate-schema-sync.sh
vendored
Executable file
63
.github/workflows/scripts/validate-schema-sync.sh
vendored
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Validate that Go config types in transports/bifrost-http/lib/config.go
|
||||||
|
# stay in sync (fields + enum values) with transports/config.schema.json.
|
||||||
|
# Walks the type graph recursively via go/types rather than regex-parsing source.
|
||||||
|
|
||||||
|
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
|
||||||
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
|
else
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
fi
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||||
|
TOOL_DIR="$SCRIPT_DIR/schemasync"
|
||||||
|
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
if ! command -v go >/dev/null 2>&1; then
|
||||||
|
echo "❌ go toolchain required for schema-sync validation"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure go.work exists at the repo root. schemasync's packages.Load needs
|
||||||
|
# it to resolve bifrost's local modules against each other. On fresh CI
|
||||||
|
# runners go.work is not checked in, so we provision it here inline.
|
||||||
|
# Sibling scripts (test-bifrost-http.sh etc.) call setup-go-workspace.sh
|
||||||
|
# via `source`, but that relies on the `return` builtin which has
|
||||||
|
# platform-dependent edge cases under `set -e`; we instead do the same
|
||||||
|
# work inline so this wrapper is self-contained.
|
||||||
|
if [ ! -f "$REPO_ROOT/go.work" ]; then
|
||||||
|
echo "🔧 Setting up Go workspace (go.work not found)..."
|
||||||
|
(
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
go work init
|
||||||
|
for mod in ./core ./framework \
|
||||||
|
./plugins/compat ./plugins/governance ./plugins/jsonparser \
|
||||||
|
./plugins/logging ./plugins/maxim ./plugins/mocker \
|
||||||
|
./plugins/otel ./plugins/prompts ./plugins/semanticcache \
|
||||||
|
./plugins/telemetry \
|
||||||
|
./transports ./cli; do
|
||||||
|
if [ -f "$REPO_ROOT/$mod/go.mod" ]; then
|
||||||
|
go work use "$mod"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
)
|
||||||
|
echo "✅ Go workspace initialized at $REPO_ROOT/go.work"
|
||||||
|
else
|
||||||
|
echo "🔍 Go workspace already exists at $REPO_ROOT/go.work, skipping initialization"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔍 Validating Go ↔ config.schema.json sync (recursive, AST-based)"
|
||||||
|
echo "=================================================================="
|
||||||
|
|
||||||
|
# The schemasync tool is its own module (separate go.mod). Build it with
|
||||||
|
# GOWORK=off so the tool's deps (golang.org/x/tools) resolve against its
|
||||||
|
# own go.mod, not the repo's go.work. At runtime the tool itself sets
|
||||||
|
# GOWORK=<repo-root>/go.work when loading bifrost packages.
|
||||||
|
(cd "$TOOL_DIR" && GOWORK=off go build -o /tmp/schemasync .)
|
||||||
|
/tmp/schemasync \
|
||||||
|
--schema "$REPO_ROOT/transports/config.schema.json" \
|
||||||
|
--pkg-root "$REPO_ROOT" \
|
||||||
|
--helm-values "$REPO_ROOT/helm-charts/bifrost/values.schema.json" \
|
||||||
|
--helm-helpers "$REPO_ROOT/helm-charts/bifrost/templates/_helpers.tpl"
|
||||||
73
.github/workflows/scripts/verify-bifrost-http-release.sh
vendored
Executable file
73
.github/workflows/scripts/verify-bifrost-http-release.sh
vendored
Executable file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script to verify if bifrost-http was successfully released
|
||||||
|
# This ensures Docker images are only built after a successful bifrost-http release
|
||||||
|
# Exits with code 0 if release is verified or not needed, exits with code 78 to skip if release failed
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
VERSION=$1
|
||||||
|
RELEASE_NEEDED=$2
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo "❌ Error: Version not provided"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If release was not needed, skip verification
|
||||||
|
if [ "$RELEASE_NEEDED" = "false" ]; then
|
||||||
|
echo "ℹ️ Bifrost-http release was not needed, skipping verification"
|
||||||
|
echo " Docker images will be built with existing version"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔍 Verifying bifrost-http release v${VERSION}..."
|
||||||
|
|
||||||
|
# Check if the git tag exists
|
||||||
|
if ! git rev-parse "transports/bifrost-http/v${VERSION}" >/dev/null 2>&1; then
|
||||||
|
echo "⚠️ Git tag transports/bifrost-http/v${VERSION} not found"
|
||||||
|
echo " Bifrost-http release did not complete successfully"
|
||||||
|
echo " Skipping Docker image build..."
|
||||||
|
exit 78 # Exit code 78 will be used to skip the job
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Git tag found: transports/bifrost-http/v${VERSION}"
|
||||||
|
|
||||||
|
# Check if the GitHub release exists
|
||||||
|
if [ -n "$GH_TOKEN" ]; then
|
||||||
|
echo "🔍 Checking GitHub release..."
|
||||||
|
if gh release view "transports/bifrost-http/v${VERSION}" >/dev/null 2>&1; then
|
||||||
|
echo "✅ GitHub release found for transports/bifrost-http/v${VERSION}"
|
||||||
|
else
|
||||||
|
echo "⚠️ GitHub release for transports/bifrost-http/v${VERSION} not found"
|
||||||
|
echo " Bifrost-http release did not complete successfully"
|
||||||
|
echo " Skipping Docker image build..."
|
||||||
|
exit 78 # Exit code 78 will be used to skip the job
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⚠️ Warning: GH_TOKEN not set, skipping GitHub release check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if dist binaries exist for the version
|
||||||
|
echo "🔍 Checking if release binaries exist..."
|
||||||
|
BINARY_FOUND=false
|
||||||
|
|
||||||
|
# Check for common binary paths
|
||||||
|
for arch in "darwin/amd64" "darwin/arm64" "linux/amd64"; do
|
||||||
|
BINARY_PATH="dist/${arch}/bifrost-http"
|
||||||
|
if [ -f "$BINARY_PATH" ]; then
|
||||||
|
echo "✅ Found binary: $BINARY_PATH"
|
||||||
|
BINARY_FOUND=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$BINARY_FOUND" = false ]; then
|
||||||
|
echo "⚠️ Warning: No release binaries found in dist/, but continuing..."
|
||||||
|
echo " This might be expected if binaries are uploaded to external storage"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Verification complete: bifrost-http v${VERSION} was successfully released"
|
||||||
|
echo " Proceeding with Docker image build..."
|
||||||
|
|
||||||
159
.github/workflows/snyk.yml
vendored
Normal file
159
.github/workflows/snyk.yml
vendored
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
name: Snyk checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, master, "**/*"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["**/*"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
snyk-open-source:
|
||||||
|
name: Snyk Open Source (deps)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: block
|
||||||
|
allowed-endpoints: >
|
||||||
|
api.github.com:443
|
||||||
|
api.snyk.io:443
|
||||||
|
downloads.snyk.io:443
|
||||||
|
files.pythonhosted.org:443
|
||||||
|
fonts.googleapis.com:443
|
||||||
|
fonts.gstatic.com:443
|
||||||
|
github.com:443
|
||||||
|
iojs.org:443
|
||||||
|
nodejs.org:443
|
||||||
|
packages.microsoft.com:443
|
||||||
|
proxy.golang.org:443
|
||||||
|
raw.githubusercontent.com:443
|
||||||
|
registry.npmjs.org:443
|
||||||
|
release-assets.githubusercontent.com:443
|
||||||
|
releases.astral.sh:443
|
||||||
|
static.snyk.io:443
|
||||||
|
storage.googleapis.com:443
|
||||||
|
sum.golang.org:443
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Setup Node (for UI)
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version: "25"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||||
|
with:
|
||||||
|
version: "0.11.0"
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
- name: Sync Python dependencies (integrations)
|
||||||
|
working-directory: tests/integrations/python
|
||||||
|
run: uv sync --frozen
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
|
with:
|
||||||
|
go-version: "1.26.2"
|
||||||
|
|
||||||
|
- name: Setup Go workspace
|
||||||
|
run: make setup-workspace
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: make build LOCAL=1
|
||||||
|
|
||||||
|
- name: Install Snyk CLI
|
||||||
|
uses: maximhq/snyk-actions/setup@9adf32b1121593767fc3c057af55b55db032dc04 # v1.0.0
|
||||||
|
with:
|
||||||
|
snyk-version: v1.1303.2
|
||||||
|
|
||||||
|
- name: Snyk test (all projects)
|
||||||
|
env:
|
||||||
|
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||||
|
run: snyk test --all-projects --detection-depth=4 --exclude=examples,tests --sarif-file-output=snyk.sarif || true
|
||||||
|
|
||||||
|
- name: Upload SARIF
|
||||||
|
if: always() && hashFiles('snyk.sarif') != ''
|
||||||
|
uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||||
|
with:
|
||||||
|
sarif_file: snyk.sarif
|
||||||
|
|
||||||
|
snyk-code:
|
||||||
|
name: Snyk Code (SAST)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||||
|
with:
|
||||||
|
egress-policy: block
|
||||||
|
allowed-endpoints: >
|
||||||
|
api.github.com:443
|
||||||
|
api.snyk.io:443
|
||||||
|
deeproxy.snyk.io:443
|
||||||
|
downloads.snyk.io:443
|
||||||
|
files.pythonhosted.org:443
|
||||||
|
fonts.googleapis.com:443
|
||||||
|
fonts.gstatic.com:443
|
||||||
|
github.com:443
|
||||||
|
iojs.org:443
|
||||||
|
nodejs.org:443
|
||||||
|
packages.microsoft.com:443
|
||||||
|
proxy.golang.org:443
|
||||||
|
raw.githubusercontent.com:443
|
||||||
|
registry.npmjs.org:443
|
||||||
|
release-assets.githubusercontent.com:443
|
||||||
|
releases.astral.sh:443
|
||||||
|
storage.googleapis.com:443
|
||||||
|
sum.golang.org:443
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Setup Node (for UI)
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version: "25"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||||
|
with:
|
||||||
|
version: "0.11.0"
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
- name: Sync Python dependencies (integrations)
|
||||||
|
working-directory: tests/integrations/python
|
||||||
|
run: uv sync --frozen
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
|
with:
|
||||||
|
go-version: "1.26.2"
|
||||||
|
|
||||||
|
- name: Setup Go workspace
|
||||||
|
run: make setup-workspace
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: make build LOCAL=1
|
||||||
|
|
||||||
|
- name: Install Snyk CLI
|
||||||
|
uses: maximhq/snyk-actions/setup@9adf32b1121593767fc3c057af55b55db032dc04 # v1.0.0
|
||||||
|
with:
|
||||||
|
snyk-version: v1.1303.2
|
||||||
|
|
||||||
|
- name: Snyk Code test
|
||||||
|
env:
|
||||||
|
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||||
|
run: snyk code test --sarif-file-output=snyk-code.sarif || true
|
||||||
|
|
||||||
|
- name: Upload SARIF
|
||||||
|
if: always() && hashFiles('snyk-code.sarif') != ''
|
||||||
|
uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||||
|
with:
|
||||||
|
sarif_file: snyk-code.sarif
|
||||||
173
.gitignore
vendored
Normal file
173
.gitignore
vendored
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
.env
|
||||||
|
.vscode
|
||||||
|
.DS_Store
|
||||||
|
*_creds*
|
||||||
|
**/venv/
|
||||||
|
**/__pycache__/**
|
||||||
|
private.*
|
||||||
|
.venv
|
||||||
|
bifrost-data
|
||||||
|
test-coverage-local.sh
|
||||||
|
|
||||||
|
# Enterprise
|
||||||
|
ui/app/enterprise
|
||||||
|
ui/components/enterprise
|
||||||
|
|
||||||
|
# Temporary directories
|
||||||
|
**/temp/
|
||||||
|
/transports/ui
|
||||||
|
/transports/bifrost-http/lib/ui
|
||||||
|
/transports/bifrost-http/ui/
|
||||||
|
transports/bifrost-http/logs/
|
||||||
|
transports/bifrost-http/tmp/
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
**/dist
|
||||||
|
**/tmp/
|
||||||
|
/temp*/
|
||||||
|
!examples/mcps/temperature/
|
||||||
|
tmp/
|
||||||
|
tmp-*
|
||||||
|
.tmp
|
||||||
|
private
|
||||||
|
**/bin
|
||||||
|
build/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Go workspaces (local only)
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# Generated schema copy (created by go generate)
|
||||||
|
transports/schema/config.schema.json
|
||||||
|
|
||||||
|
# Sqlite DBs
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
|
||||||
|
# Test reports
|
||||||
|
test-reports
|
||||||
|
tests/e2e/api/newman-reports
|
||||||
|
|
||||||
|
# AI Agent specific
|
||||||
|
settings.local.json
|
||||||
|
.codex/
|
||||||
|
.cursor/
|
||||||
|
.claude/*
|
||||||
|
!.claude/skills/
|
||||||
|
|
||||||
|
# Python specific
|
||||||
|
**/__pycache__/**
|
||||||
|
**/venv/
|
||||||
|
**/.venv/
|
||||||
|
**/.pytest_cache/
|
||||||
|
**/.coverage/
|
||||||
|
**/.pytest_cache/
|
||||||
|
|
||||||
|
|
||||||
|
# Nix specific
|
||||||
|
.direnv
|
||||||
|
.nix-store
|
||||||
|
|
||||||
|
# MCP servers
|
||||||
|
examples/mcps/**/bin/
|
||||||
|
examples/mcps/http-no-ping-server/http-server
|
||||||
|
examples/mcps/test-tools-server/dist/
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Test results
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
blob-report/
|
||||||
|
|
||||||
|
# Screenshots and videos
|
||||||
|
screenshots/
|
||||||
|
videos/
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Debug logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
playwright/.cache/
|
||||||
|
|
||||||
|
# Terraform
|
||||||
|
.terraform/
|
||||||
|
.terraform.lock.hcl
|
||||||
|
terraform.tfstate
|
||||||
|
terraform.tfstate.backup
|
||||||
|
*.tfvars
|
||||||
|
!*.tfvars.example
|
||||||
|
|
||||||
|
# Bifrost benchmarking
|
||||||
|
bifrost-benchmarking
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
:memory:
|
||||||
|
|
||||||
|
# Generated test TLS certs (created by tests/docker-compose.yml redis-certs-init)
|
||||||
|
tests/redis-certs/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
ui/node_modules
|
||||||
|
ui/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
ui/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
ui/.next/
|
||||||
|
ui/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# auto-generated TanStack Router route tree
|
||||||
|
ui/app/routeTree.gen.ts
|
||||||
|
|
||||||
|
.tanstack
|
||||||
|
.next
|
||||||
31
.pre-commit-config.yaml
Normal file
31
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/gitleaks/gitleaks
|
||||||
|
rev: v8.16.3
|
||||||
|
hooks:
|
||||||
|
- id: gitleaks
|
||||||
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
|
rev: v2.9.0
|
||||||
|
hooks:
|
||||||
|
- id: golangci-lint
|
||||||
|
- repo: https://github.com/jumanjihouse/pre-commit-hooks
|
||||||
|
rev: 3.0.0
|
||||||
|
hooks:
|
||||||
|
- id: shellcheck
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: eslint
|
||||||
|
name: eslint (ui)
|
||||||
|
language: system
|
||||||
|
entry: ui/node_modules/.bin/eslint
|
||||||
|
args: [-c, ui/eslint.config.mjs, --max-warnings=0, --fix]
|
||||||
|
files: ^ui/.*\.(mjs|cjs|jsx|tsx?|js)$
|
||||||
|
exclude: ^ui/node_modules/
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.4.0
|
||||||
|
hooks:
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- repo: https://github.com/pylint-dev/pylint
|
||||||
|
rev: v2.17.2
|
||||||
|
hooks:
|
||||||
|
- id: pylint
|
||||||
7
.snyk
Normal file
7
.snyk
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Snyk (https://snyk.io) policy file
|
||||||
|
# Manages vulnerability ignores and patches for this repository.
|
||||||
|
version: v1.25.0
|
||||||
|
ignore: {
|
||||||
|
"tests"
|
||||||
|
}
|
||||||
|
patch: {}
|
||||||
930
AGENTS.md
Normal file
930
AGENTS.md
Normal file
@@ -0,0 +1,930 @@
|
|||||||
|
# AGENTS.md — Bifrost AI Gateway
|
||||||
|
|
||||||
|
> Context for AI agents (Claude Code, Copilot, Cursor, etc.) working on this codebase. Read this fully before making changes.
|
||||||
|
|
||||||
|
## What is Bifrost?
|
||||||
|
|
||||||
|
Bifrost is a high-performance AI gateway that unifies 20+ LLM providers behind a single OpenAI-compatible API with ~11µs overhead at 5,000 RPS. It also serves as an MCP (Model Context Protocol) gateway, turning static chat models into tool-calling agents.
|
||||||
|
|
||||||
|
GitHub: `maximhq/bifrost`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repository Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
bifrost/
|
||||||
|
├── core/ # Go core library — the engine
|
||||||
|
│ ├── bifrost.go # Main struct, request queuing, provider lifecycle (~3.4K lines)
|
||||||
|
│ ├── inference.go # Inference routing, fallbacks, streaming dispatch (~1.9K lines)
|
||||||
|
│ ├── mcp.go # MCP integration entry point
|
||||||
|
│ ├── schemas/ # ALL shared Go types — 41 files
|
||||||
|
│ │ ├── bifrost.go # BifrostConfig, ModelProvider enum, RequestType enum, context keys
|
||||||
|
│ │ ├── provider.go # Provider interface (30+ methods), NetworkConfig, ProviderConfig
|
||||||
|
│ │ ├── plugin.go # LLMPlugin, MCPPlugin, HTTPTransportPlugin, ObservabilityPlugin
|
||||||
|
│ │ ├── context.go # BifrostContext (custom context.Context with mutable values)
|
||||||
|
│ │ ├── chatcompletions.go # Chat completion request/response types
|
||||||
|
│ │ ├── responses.go # OpenAI Responses API types
|
||||||
|
│ │ ├── embedding.go # Embedding types
|
||||||
|
│ │ ├── images.go # Image generation types
|
||||||
|
│ │ ├── batch.go # Batch operation types
|
||||||
|
│ │ ├── files.go # File management types
|
||||||
|
│ │ ├── mcp.go # MCP types
|
||||||
|
│ │ ├── trace.go # Tracer interface
|
||||||
|
│ │ └── logger.go # Logger interface
|
||||||
|
│ ├── providers/ # 20+ provider implementations
|
||||||
|
│ │ ├── openai/ # Reference implementation (largest, most complete)
|
||||||
|
│ │ ├── anthropic/ # Non-OpenAI-compatible example
|
||||||
|
│ │ ├── bedrock/ # AWS event-stream protocol
|
||||||
|
│ │ ├── gemini/ # Google-specific API shape
|
||||||
|
│ │ ├── groq/ # OpenAI-compatible (minimal, delegates to openai/)
|
||||||
|
│ │ └── utils/ # Shared: HTTP client, SSE parsing, error handling, scanner pool
|
||||||
|
│ ├── pool/ # Generic Pool[T] — dual-mode (prod: sync.Pool, debug: full tracking)
|
||||||
|
│ │ ├── pool_prod.go # Zero-overhead sync.Pool wrapper (default build)
|
||||||
|
│ │ └── pool_debug.go # Double-release/use-after-release/leak detection (-tags pooldebug)
|
||||||
|
│ ├── mcp/ # MCP protocol implementation
|
||||||
|
│ │ ├── agent.go # Agent orchestration loop (multi-turn tool calling)
|
||||||
|
│ │ ├── clientmanager.go # MCP client lifecycle management
|
||||||
|
│ │ ├── toolmanager.go # Tool registration, discovery, filtering
|
||||||
|
│ │ ├── healthmonitor.go # Client health monitoring
|
||||||
|
│ │ └── codemode/starlark/ # Starlark sandbox for code-mode execution
|
||||||
|
│ └── internal/
|
||||||
|
│ ├── llmtests/ # LLM integration test infra (48 files, scenario-based)
|
||||||
|
│ └── mcptests/ # MCP/Agent test infra (40+ files, mock-based)
|
||||||
|
│
|
||||||
|
├── framework/ # Data persistence, streaming, ecosystem utilities
|
||||||
|
│ ├── configstore/ # Config storage backends (file, postgres)
|
||||||
|
│ ├── logstore/ # Log storage backends (file, postgres)
|
||||||
|
│ ├── vectorstore/ # Vector storage (Weaviate, Qdrant, Redis, Pinecone)
|
||||||
|
│ ├── streaming/ # Streaming accumulator, delta copying, response marshaling
|
||||||
|
│ │ ├── accumulator.go # Chunk accumulation into full response (~24KB)
|
||||||
|
│ │ ├── chat.go # Chat stream handling (~17KB)
|
||||||
|
│ │ └── responses.go # Response stream marshaling (~35KB)
|
||||||
|
│ ├── modelcatalog/ # Model metadata registry
|
||||||
|
│ ├── tracing/ # Distributed tracing helpers
|
||||||
|
│ └── encrypt/ # Encryption utilities
|
||||||
|
│
|
||||||
|
├── transports/
|
||||||
|
│ ├── config.schema.json # JSON Schema — THE source of truth for config.json (~2700 lines)
|
||||||
|
│ └── bifrost-http/ # HTTP gateway transport
|
||||||
|
│ ├── server/ # Server lifecycle, route registration
|
||||||
|
│ ├── handlers/ # 27 HTTP endpoint handlers
|
||||||
|
│ │ ├── inference.go # Chat/text completions, responses API (~109KB)
|
||||||
|
│ │ ├── mcpinference.go # MCP tool execution
|
||||||
|
│ │ ├── governance.go # Virtual keys, teams, customers, budgets (~100KB)
|
||||||
|
│ │ ├── providers.go # Provider CRUD, key management
|
||||||
|
│ │ ├── mcp.go # MCP client registry management
|
||||||
|
│ │ ├── logging.go # Log queries, stats, histograms
|
||||||
|
│ │ ├── config.go # System configuration
|
||||||
|
│ │ ├── plugins.go # Plugin CRUD
|
||||||
|
│ │ ├── cache.go # Cache management
|
||||||
|
│ │ ├── session.go # Auth/session management
|
||||||
|
│ │ ├── health.go # Health checks
|
||||||
|
│ │ ├── mcpserver.go # MCP server (SSE/streamable HTTP)
|
||||||
|
│ │ ├── websocket.go # WebSocket handler
|
||||||
|
│ │ ├── devpprof.go # Pool debug profiler endpoint (~23KB)
|
||||||
|
│ │ └── middlewares.go # Middleware definitions
|
||||||
|
│ ├── lib/ # ChainMiddlewares, config, context conversion
|
||||||
|
│ └── integrations/ # SDK compatibility layers
|
||||||
|
│ ├── openai.go # OpenAI SDK drop-in compatibility
|
||||||
|
│ ├── anthropic.go # Anthropic SDK compatibility
|
||||||
|
│ ├── bedrock.go # AWS Bedrock SDK compatibility
|
||||||
|
│ ├── genai.go # Google GenAI SDK compatibility
|
||||||
|
│ ├── langchain.go # LangChain compatibility
|
||||||
|
│ ├── litellm.go # LiteLLM compatibility
|
||||||
|
│ └── pydanticai.go # PydanticAI compatibility
|
||||||
|
│
|
||||||
|
├── plugins/ # Go plugins — each has own go.mod
|
||||||
|
│ ├── governance/ # Budget, rate limiting, virtual keys, routing, RBAC
|
||||||
|
│ ├── telemetry/ # Prometheus metrics, push gateway
|
||||||
|
│ ├── logging/ # Request/response audit logging
|
||||||
|
│ ├── semanticcache/ # Semantic response caching via vector store
|
||||||
|
│ ├── otel/ # OpenTelemetry tracing
|
||||||
|
│ ├── mocker/ # Mock responses for testing
|
||||||
|
│ ├── jsonparser/ # JSON extraction utilities
|
||||||
|
│ ├── maxim/ # Maxim observability
|
||||||
|
│ └── compat/ # LiteLLM SDK compatibility (HTTP transport)
|
||||||
|
│
|
||||||
|
├── ui/ # React + vite web interface
|
||||||
|
│ ├── app/workspace/ # Feature pages (20+ workspace sections)
|
||||||
|
│ ├── components/ # Shared React components
|
||||||
|
│ └── lib/ # Constants, utilities, types
|
||||||
|
│
|
||||||
|
├── tests/e2e/ # Playwright E2E tests
|
||||||
|
│ ├── core/ # Fixtures, page objects, helpers, API actions
|
||||||
|
│ └── features/ # Per-feature test suites
|
||||||
|
│
|
||||||
|
├── docs/ # Mintlify MDX documentation
|
||||||
|
│ ├── docs.json # Navigation config
|
||||||
|
│ ├── media/ # Screenshots (ui-*.png naming convention)
|
||||||
|
│ └── (architecture|features|providers|mcp|plugins|enterprise|...)
|
||||||
|
│
|
||||||
|
├── .claude/skills/ # Claude Code skill definitions (4 skills)
|
||||||
|
├── go.work # Go workspace — requires Go 1.26.1
|
||||||
|
├── Makefile # Build, test, dev commands (1300+ lines)
|
||||||
|
└── terraform/ # Infrastructure as Code
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Go Workspace
|
||||||
|
|
||||||
|
Bifrost is a **multi-module Go workspace**. Each module has its own `go.mod`:
|
||||||
|
|
||||||
|
```
|
||||||
|
go.work
|
||||||
|
├── core/go.mod # github.com/maximhq/bifrost/core
|
||||||
|
├── framework/go.mod # github.com/maximhq/bifrost/framework
|
||||||
|
├── transports/go.mod # github.com/maximhq/bifrost/transports
|
||||||
|
└── plugins/*/go.mod # 9 plugin modules (governance, telemetry, logging, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
- Run `go mod tidy` in the **specific module directory**, not the root
|
||||||
|
- Cross-module imports resolve via workspace locally, but need explicit `require` in `go.mod` for releases
|
||||||
|
- The workspace requires **Go 1.26.1** (`go.work` directive)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build, Test & Dev Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
make dev # Full local dev (UI + API with hot reload via air)
|
||||||
|
make build # Build bifrost-http binary
|
||||||
|
|
||||||
|
# Core tests (provider integration tests — hit live APIs)
|
||||||
|
make test-core # All providers
|
||||||
|
make test-core PROVIDER=openai # Specific provider
|
||||||
|
make test-core PROVIDER=openai TESTCASE=TestSimpleChat # Specific test
|
||||||
|
make test-core PATTERN=TestStreaming # Tests matching pattern
|
||||||
|
make test-core DEBUG=1 # With Delve debugger on :2345
|
||||||
|
|
||||||
|
# MCP/Agent tests (mock-based, no live APIs)
|
||||||
|
make test-mcp # All MCP tests
|
||||||
|
make test-mcp TESTCASE=TestAgentLoop # Specific test
|
||||||
|
make test-mcp TYPE=agent # By category (agent|tool|connection|codemode)
|
||||||
|
|
||||||
|
# Plugin tests
|
||||||
|
make test-plugins # All plugins
|
||||||
|
make test-governance # Governance plugin specifically
|
||||||
|
|
||||||
|
# Integration tests (SDK compatibility)
|
||||||
|
make test-integrations-py # Python SDK tests
|
||||||
|
make test-integrations-ts # TypeScript SDK tests
|
||||||
|
|
||||||
|
# E2E tests (Playwright, requires running dev server)
|
||||||
|
make run-e2e # All E2E tests
|
||||||
|
make run-e2e FLOW=providers # Specific feature
|
||||||
|
|
||||||
|
# Code quality
|
||||||
|
make lint # Linting
|
||||||
|
make fmt # Format code
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Request Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Client HTTP Request
|
||||||
|
→ FastHTTP Transport (parsing, validation ~2µs)
|
||||||
|
→ SDK Integration Layer (OpenAI/Anthropic/Bedrock format → Bifrost format)
|
||||||
|
→ Middleware Chain (lib.ChainMiddlewares, applied per-route)
|
||||||
|
→ HTTPTransportPreHook (HTTP-level plugins, can short-circuit)
|
||||||
|
→ PreLLMHook Pipeline (auth, rate-limit, cache check — registration order)
|
||||||
|
→ MCP Tool Discovery & Injection (if tool_choice present)
|
||||||
|
→ Provider Queue (channel-based, per-provider isolation)
|
||||||
|
→ Worker picks up request
|
||||||
|
→ Key Selection (~10ns weighted random)
|
||||||
|
→ Provider API Call (fasthttp client, connection pooling)
|
||||||
|
→ Response / SSE Stream
|
||||||
|
→ PostLLMHook Pipeline (reverse order of PreLLMHooks)
|
||||||
|
→ Tool Execution Loop (if tool_calls in response, MCP agent loop)
|
||||||
|
→ HTTPTransportPostHook (reverse order)
|
||||||
|
→ Response Serialization
|
||||||
|
→ HTTP Response to Client
|
||||||
|
```
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
|
||||||
|
- **Provider isolation**: Each provider has its own worker pool and queue. One provider going down doesn't cascade to others.
|
||||||
|
- **Channel-based async**: Request routing uses Go channels (`chan *ChannelMessage`), not mutexes. The `ProviderQueue` struct manages channel lifecycle with atomic flags.
|
||||||
|
- **Object pooling everywhere**: `sync.Pool` wrappers reduce GC pressure. Pools exist for: channel messages, response channels, error channels, stream channels, plugin pipelines, MCP requests, HTTP request/response objects, scanner buffers.
|
||||||
|
- **Plugin pipeline symmetry**: Pre-hooks execute in registration order, post-hooks in **reverse** order (LIFO). For every pre-hook executed, the corresponding post-hook is guaranteed to run.
|
||||||
|
- **Streaming**: SSE chunks flow through `chan chan *schemas.BifrostStreamChunk`. Accumulated into full response for post-hooks via `framework/streaming/accumulator.go`.
|
||||||
|
|
||||||
|
### BifrostContext — Custom Context
|
||||||
|
|
||||||
|
`BifrostContext` (`core/schemas/context.go`) is a custom `context.Context` with **thread-safe mutable values**. Unlike standard Go contexts, values can be set after creation:
|
||||||
|
|
||||||
|
```go
|
||||||
|
ctx := schemas.NewBifrostContext(parent, deadline)
|
||||||
|
ctx.SetValue(key, value) // Thread-safe, uses RWMutex
|
||||||
|
ctx.WithValue(key, value) // Chainable variant
|
||||||
|
```
|
||||||
|
|
||||||
|
**Reserved context keys** (set by Bifrost internals — DO NOT set manually):
|
||||||
|
- `BifrostContextKeySelectedKeyID/Name` — Set by governance plugin
|
||||||
|
- `BifrostContextKeyGovernance*` — Set by governance plugin
|
||||||
|
- `BifrostContextKeyNumberOfRetries`, `BifrostContextKeyFallbackIndex` — Set by retry/fallback logic
|
||||||
|
- `BifrostContextKeyStreamEndIndicator` — Set by streaming infrastructure
|
||||||
|
- `BifrostContextKeyTrace*`, `BifrostContextKeySpan*` — Set by tracing middleware
|
||||||
|
|
||||||
|
**User-settable keys** (plugins and handlers can set these):
|
||||||
|
- `BifrostContextKeyVirtualKey` (`x-bf-vk`) — Virtual key for governance
|
||||||
|
- `BifrostContextKeyAPIKeyName` (`x-bf-api-key`) — Explicit key selection by name
|
||||||
|
- `BifrostContextKeyAPIKeyID` (`x-bf-api-key-id`) — Explicit key selection by ID (takes priority over name)
|
||||||
|
- `BifrostContextKeyRequestID` — Request ID
|
||||||
|
- `BifrostContextKeyExtraHeaders` — Extra headers to forward to provider
|
||||||
|
- `BifrostContextKeyURLPath` — Custom URL path for provider
|
||||||
|
- `BifrostContextKeySkipKeySelection` — Skip key selection (pass empty key)
|
||||||
|
- `BifrostContextKeyUseRawRequestBody` — Send raw body directly to provider
|
||||||
|
|
||||||
|
**Gotcha**: `BlockRestrictedWrites()` silently drops writes to reserved keys. This prevents plugins from accidentally overwriting internal state.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Patterns
|
||||||
|
|
||||||
|
### Provider Implementation
|
||||||
|
|
||||||
|
There are **two categories** of providers:
|
||||||
|
|
||||||
|
**Category 1: Non-OpenAI-compatible** (Anthropic, Bedrock, Gemini, Cohere, HuggingFace, Replicate, ElevenLabs):
|
||||||
|
```
|
||||||
|
core/providers/<name>/
|
||||||
|
├── <name>.go # Controller: constructor, interface methods, HTTP orchestration
|
||||||
|
├── <name>_test.go # Tests
|
||||||
|
├── types.go # ALL provider-specific structs (PascalCase prefixed with provider name)
|
||||||
|
├── utils.go # Constants, base URLs, helpers (camelCase for unexported)
|
||||||
|
├── errors.go # Error parsing: provider HTTP error → *schemas.BifrostError
|
||||||
|
├── chat.go # Chat request/response converters
|
||||||
|
├── embedding.go # Embedding converters (if supported)
|
||||||
|
├── images.go # Image generation (if supported)
|
||||||
|
├── speech.go # TTS/STT (if supported)
|
||||||
|
└── responses.go # Responses API + streaming converters
|
||||||
|
```
|
||||||
|
|
||||||
|
**Category 2: OpenAI-compatible** (Groq, Cerebras, Ollama, Perplexity, OpenRouter, Parasail, Nebius, xAI, SGL):
|
||||||
|
```
|
||||||
|
core/providers/<name>/
|
||||||
|
├── <name>.go # Minimal — constructor + delegates to openai.HandleOpenAI* functions
|
||||||
|
└── <name>_test.go # Tests
|
||||||
|
```
|
||||||
|
|
||||||
|
**Converter function naming convention:**
|
||||||
|
- `To<ProviderName><Feature>Request()` — Bifrost schema → Provider API format
|
||||||
|
- `ToBifrost<Feature>Response()` — Provider API format → Bifrost schema
|
||||||
|
- These must be **pure transformation functions** — no HTTP calls, no logging, no side effects
|
||||||
|
|
||||||
|
**Provider constructor pattern:**
|
||||||
|
```go
|
||||||
|
func NewProvider(config schemas.ProviderConfig) (*Provider, error) {
|
||||||
|
// Validate config, set up fasthttp.Client with connection pooling
|
||||||
|
client := &fasthttp.Client{
|
||||||
|
MaxConnsPerHost: config.NetworkConfig.MaxConnsPerHost, // configurable, default 5000
|
||||||
|
MaxIdleConnDuration: 30 * time.Second,
|
||||||
|
}
|
||||||
|
// After ConfigureProxy/ConfigureDialer/ConfigureTLS, build a sibling client
|
||||||
|
// for streaming. BuildStreamingClient zeros ReadTimeout/WriteTimeout/MaxConnDuration
|
||||||
|
// so streams aren't killed by fasthttp's whole-response deadline; per-chunk idle
|
||||||
|
// is enforced at the app layer via NewIdleTimeoutReader.
|
||||||
|
streamingClient := providerUtils.BuildStreamingClient(client)
|
||||||
|
return &Provider{client: client, streamingClient: streamingClient, ...}, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Streaming vs unary client:** Every provider holds two clients — `client` for unary requests (`ReadTimeout=30s` bounds the whole response) and `streamingClient` for SSE / EventStream / chunked paths (`ReadTimeout=0`; the per-chunk `NewIdleTimeoutReader` is the only governor). Pass `provider.streamingClient` to every `Handle*Streaming` / `Handle*StreamRequest` helper and to direct `Do` calls inside `*Stream` methods. For new providers, apply the same pattern — missing the switch means streams get killed at 30s.
|
||||||
|
|
||||||
|
**Note:** Bedrock uses `net/http` (not fasthttp) with HTTP/2 support. Its `http.Transport` is configured with `ForceAttemptHTTP2: true` and `MaxConnsPerHost` from `NetworkConfig` to allow multiple HTTP/2 connections when the server's per-connection stream limit (100 for AWS Bedrock) is reached. Use `providerUtils.BuildStreamingHTTPClient(client)` to derive the streaming variant — it shares the base `Transport` (safe for concurrent reuse) but clears `Client.Timeout`.
|
||||||
|
|
||||||
|
### The Provider Interface
|
||||||
|
|
||||||
|
`core/schemas/provider.go` defines the `Provider` interface with **30+ methods**. Every provider must implement all of them (returning "not supported" for unsupported operations). The interface covers:
|
||||||
|
|
||||||
|
- `ListModels`, `ChatCompletion`, `ChatCompletionStream`
|
||||||
|
- `Responses`, `ResponsesStream` (OpenAI Responses API)
|
||||||
|
- `TextCompletion`, `TextCompletionStream`
|
||||||
|
- `Embedding`, `Speech`, `SpeechStream`, `Transcription`, `TranscriptionStream`
|
||||||
|
- `ImageGeneration`, `ImageGenerationStream`, `ImageEdit`, `ImageEditStream`, `ImageVariation`
|
||||||
|
- `CountTokens`
|
||||||
|
- `Batch*` (Create, List, Retrieve, Cancel, Results)
|
||||||
|
- `File*` (Upload, List, Retrieve, Delete, Content)
|
||||||
|
- `Container*` and `ContainerFile*` (Create, List, Retrieve, Delete, Content)
|
||||||
|
|
||||||
|
**Streaming methods** receive a `PostHookRunner` callback and return `chan *BifrostStreamChunk`:
|
||||||
|
```go
|
||||||
|
ChatCompletionStream(ctx *BifrostContext, postHookRunner PostHookRunner, key Key, request *BifrostChatRequest) (chan *BifrostStreamChunk, *BifrostError)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
Each provider has `errors.go` with an `ErrorConverter` function:
|
||||||
|
```go
|
||||||
|
type ErrorConverter func(resp *fasthttp.Response, requestType schemas.RequestType, providerName schemas.ModelProvider, model string) *schemas.BifrostError
|
||||||
|
```
|
||||||
|
|
||||||
|
The shared utility `providerUtils.HandleProviderAPIError()` handles common HTTP error parsing. Provider-specific parsers add extra field mapping. Errors always carry metadata:
|
||||||
|
```go
|
||||||
|
bifrostErr.ExtraFields.Provider = providerName
|
||||||
|
bifrostErr.ExtraFields.ModelRequested = model
|
||||||
|
bifrostErr.ExtraFields.RequestType = requestType
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin System
|
||||||
|
|
||||||
|
Four plugin interfaces exist:
|
||||||
|
|
||||||
|
| Interface | Hook Methods | When Called |
|
||||||
|
|-----------|-------------|------------|
|
||||||
|
| `LLMPlugin` | `PreLLMHook`, `PostLLMHook` | Every LLM request (SDK + HTTP) |
|
||||||
|
| `MCPPlugin` | `PreMCPHook`, `PostMCPHook` | Every MCP tool execution |
|
||||||
|
| `HTTPTransportPlugin` | `HTTPTransportPreHook`, `HTTPTransportPostHook`, `HTTPTransportStreamChunkHook` | HTTP gateway only (not Go SDK) |
|
||||||
|
| `ObservabilityPlugin` | `Inject(ctx, trace)` | Async, after response written to wire |
|
||||||
|
|
||||||
|
**Key plugin behaviors:**
|
||||||
|
- Plugin errors are **logged as warnings**, never returned to the caller
|
||||||
|
- Pre-hooks can **short-circuit** by returning `*LLMPluginShortCircuit` (cache hit, auth failure, rate limit)
|
||||||
|
- Post-hooks receive both response and error — either can be nil. Plugins can **recover from errors** (set error to nil, provide response) or **invalidate responses** (set response to nil, provide error)
|
||||||
|
- `BifrostError.AllowFallbacks` controls whether fallback providers are tried: `nil` or `&true` = allow, `&false` = block
|
||||||
|
- `HTTPTransportStreamChunkHook` is called **per-chunk** during streaming — can modify, skip, or abort the stream
|
||||||
|
|
||||||
|
### Pool System
|
||||||
|
|
||||||
|
`core/pool/` provides `Pool[T]` with two build modes:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Production (default): zero-overhead sync.Pool wrapper
|
||||||
|
// Debug (-tags pooldebug): tracks double-release, use-after-release, leaks with stack traces
|
||||||
|
p := pool.New[MyType]("descriptive-name", func() *MyType { return &MyType{} })
|
||||||
|
obj := p.Get()
|
||||||
|
// ... use obj ...
|
||||||
|
// MUST reset ALL fields before Put — pool does not auto-reset
|
||||||
|
p.Put(obj)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Acquire/Release pattern** for types with complex reset logic (used in `schemas/plugin.go`):
|
||||||
|
```go
|
||||||
|
req := schemas.AcquireHTTPRequest() // Get from pool, pre-allocated maps
|
||||||
|
defer schemas.ReleaseHTTPRequest(req) // Clears all maps and fields, returns to pool
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP Transport Layer
|
||||||
|
|
||||||
|
**Handler pattern:** Handlers are structs with injected dependencies:
|
||||||
|
```go
|
||||||
|
type CompletionHandler struct {
|
||||||
|
client *bifrost.Bifrost
|
||||||
|
handlerStore lib.HandlerStore
|
||||||
|
config *lib.Config
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Route registration:** Each handler implements `RegisterRoutes(router, middlewares...)` — routes get middleware chains applied per-route via `lib.ChainMiddlewares()`.
|
||||||
|
|
||||||
|
**SDK integration layers** (`transports/bifrost-http/integrations/`) provide request/response converters between provider-native SDK formats and Bifrost's internal format. This enables drop-in replacement of OpenAI SDK, Anthropic SDK, AWS Bedrock SDK, Google GenAI SDK, LangChain, and LiteLLM.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
### 1. Always Reset Pooled Objects Before Put
|
||||||
|
|
||||||
|
Every pooled object must have **all** fields zeroed before `pool.Put()`. Stale data leaks between requests. The debug build catches double-release and use-after-release but **not** missing resets.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// WRONG — stale data from previous request leaks to next user
|
||||||
|
pool.Put(msg)
|
||||||
|
|
||||||
|
// RIGHT
|
||||||
|
msg.Response = nil
|
||||||
|
msg.Error = nil
|
||||||
|
msg.Context = nil
|
||||||
|
msg.ResponseStream = nil
|
||||||
|
pool.Put(msg)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Channel Lifecycle — ProviderQueue Pattern
|
||||||
|
|
||||||
|
`ProviderQueue` uses atomic flags and `sync.Once` to prevent "send on closed channel" panics:
|
||||||
|
```go
|
||||||
|
type ProviderQueue struct {
|
||||||
|
queue chan *ChannelMessage
|
||||||
|
done chan struct{}
|
||||||
|
closing uint32 // atomic: 0=open, 1=closing
|
||||||
|
signalOnce sync.Once // ensure signal fires only once
|
||||||
|
closeOnce sync.Once // ensure close fires only once
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Always check the atomic closing flag before sending. Never close a channel without this pattern.
|
||||||
|
|
||||||
|
### 3. NetworkConfig Duration Serialization
|
||||||
|
|
||||||
|
`RetryBackoffInitial` and `RetryBackoffMax` are `time.Duration` (nanoseconds) in Go but **milliseconds** (integers) in JSON. Custom `MarshalJSON`/`UnmarshalJSON` handles conversion. If adding new duration fields to any config struct, follow this pattern exactly.
|
||||||
|
|
||||||
|
### 4. ExtraHeaders — Defensive Map Copy
|
||||||
|
|
||||||
|
`NetworkConfig.ExtraHeaders` is deep-copied in `CheckAndSetDefaults()` to prevent data races between concurrent requests. Apply the same `maps.Copy()` pattern to any new map fields in config structs.
|
||||||
|
|
||||||
|
### 5. Provider Interface Has 30+ Methods
|
||||||
|
|
||||||
|
Adding a new operation type requires changes across the entire codebase:
|
||||||
|
1. Add method to `Provider` interface in `core/schemas/provider.go`
|
||||||
|
2. Implement in **all** 20+ providers (most return "not supported")
|
||||||
|
3. Add `RequestType` constant in `core/schemas/bifrost.go`
|
||||||
|
4. Add to `AllowedRequests` struct and `IsOperationAllowed()` switch
|
||||||
|
5. Add handler endpoint in `transports/bifrost-http/handlers/`
|
||||||
|
6. Wire up in `core/bifrost.go` and `core/inference.go`
|
||||||
|
|
||||||
|
### 6. OpenAI Provider Changes Cascade to 9+ Providers
|
||||||
|
|
||||||
|
Groq, Cerebras, Ollama, Perplexity, OpenRouter, Parasail, Nebius, xAI, and SGL all delegate to `openai.HandleOpenAI*` functions. **Any change to OpenAI converter logic affects all of them.** Always test broadly: `make test-core` (all providers).
|
||||||
|
|
||||||
|
### 7. Scanner Buffer Pool Has a Capacity Cap
|
||||||
|
|
||||||
|
The SSE scanner buffer pool in `core/providers/utils/utils.go` starts at 4KB. Buffers grow dynamically but those exceeding **64KB are discarded** (not returned to pool) to prevent memory bloat. Be aware when working with providers that send very large SSE events.
|
||||||
|
|
||||||
|
### 8. Plugin Execution Order is Meaningful
|
||||||
|
|
||||||
|
Pre-hooks: registration order (first registered → first to run). Post-hooks: **reverse** order. This creates "wrapping" semantics — the first plugin registered is the outermost wrapper (its pre-hook runs first, post-hook runs last). Changing registration order changes behavior.
|
||||||
|
|
||||||
|
### 9. Fallbacks Re-execute the Full Plugin Pipeline
|
||||||
|
|
||||||
|
When a provider fails and the request falls to a fallback, the **entire plugin pipeline** re-executes from scratch. Governance checks, caching, and logging all run again for each attempt. Intentional, but surprising when debugging request counts or cost tracking.
|
||||||
|
|
||||||
|
### 10. `AllowedRequests` Nil Semantics
|
||||||
|
|
||||||
|
A **nil** `*AllowedRequests` means "all operations allowed." A **non-nil** value only allows fields explicitly set to `true`. This applies to both `ProviderConfig.AllowedRequests` and `CustomProviderConfig.AllowedRequests`.
|
||||||
|
|
||||||
|
### 11. BifrostContext Reserved Keys Are Silently Dropped
|
||||||
|
|
||||||
|
When `BlockRestrictedWrites()` is active, writes to reserved keys (governance IDs, retry counts, fallback index, etc.) are **silently ignored** — no error. If your plugin needs to pass data through context, use your own custom key type.
|
||||||
|
|
||||||
|
### 12. `fasthttp`, Not `net/http`
|
||||||
|
|
||||||
|
Bifrost uses `github.com/valyala/fasthttp` for provider HTTP calls. The API is different from `net/http`:
|
||||||
|
- Use `fasthttp.AcquireRequest()`/`fasthttp.ReleaseRequest()` for lifecycle
|
||||||
|
- `fasthttp.Client` pools connections per-host (`NetworkConfig.MaxConnsPerHost`, default 5000, 30s idle)
|
||||||
|
- Request/response bodies accessed via `resp.Body()` (returns `[]byte`, not `io.Reader`)
|
||||||
|
- **Exception:** Bedrock uses `net/http` (for AWS SigV4 signing) with `http.Transport` configured for HTTP/2 multi-connection support
|
||||||
|
|
||||||
|
### 13. `sonic`, Not `encoding/json`
|
||||||
|
|
||||||
|
JSON marshaling in hot paths uses `github.com/bytedance/sonic` for performance. `core/schemas/` uses standard `encoding/json` for custom marshaling (e.g., `NetworkConfig`). Don't mix them accidentally.
|
||||||
|
|
||||||
|
### 14. Atomic Pointer for Hot Config Reload
|
||||||
|
|
||||||
|
`Bifrost` uses `atomic.Pointer` for providers and plugins lists. On updates: create new slice → atomically swap pointer. **Never mutate the slice in place** — concurrent readers would see partial state.
|
||||||
|
|
||||||
|
### 15. MCP Tool Filtering is 4 Levels Deep
|
||||||
|
|
||||||
|
Tool access follows: Global filter → Client-level filter → Tool-level filter → Per-request filter (HTTP headers). All four levels must agree for a tool to be available. Changes to filtering logic must respect this hierarchy.
|
||||||
|
|
||||||
|
### 16. `config.schema.json` is the Source of Truth
|
||||||
|
|
||||||
|
`transports/config.schema.json` (~2700 lines) is the authoritative definition for all `config.json` fields. Documentation examples must match. When adding config fields: update schema first → handlers → docs.
|
||||||
|
|
||||||
|
### 17. UI `data-testid` Attributes Are Load-Bearing
|
||||||
|
|
||||||
|
E2E tests depend on `data-testid` attributes. Convention: `data-testid="<entity>-<element>-<qualifier>"`. If you rename or remove one, search `tests/e2e/` for references. If you add new interactive elements, add `data-testid`.
|
||||||
|
|
||||||
|
### 18. E2E Tests — Never Marshal Payloads to Maps
|
||||||
|
|
||||||
|
In `tests/e2e/core/`, **never marshal API payloads to a `Record`/`Map`/plain-object and then re-serialize**. Field ordering matters for backend validation and snapshot comparisons. Construct payloads as object literals with fields in the intended order and pass directly to Playwright's `request.post({ data })`. Avoid `Object.fromEntries()`, `JSON.parse(JSON.stringify(...))` round-trips, or destructuring into an intermediate `Record<string, unknown>` — these can silently reorder fields.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding a New Provider — Full Checklist
|
||||||
|
|
||||||
|
1. Create `core/providers/<name>/` with files per the pattern (see "Provider Implementation" above)
|
||||||
|
2. Add `ModelProvider` constant in `core/schemas/bifrost.go`
|
||||||
|
3. Add to `StandardProviders` list in `core/schemas/bifrost.go`
|
||||||
|
4. Register in `core/bifrost.go` — add import + case in provider init switch
|
||||||
|
5. **UI integration** (all required):
|
||||||
|
- `ui/lib/constants/config.ts` — model placeholder + key requirement
|
||||||
|
- `ui/lib/constants/icons.tsx` — provider icon
|
||||||
|
- `ui/lib/constants/logs.ts` — provider display name (2 places)
|
||||||
|
- `docs/openapi/openapi.json` — OpenAPI spec update
|
||||||
|
- `transports/config.schema.json` — config schema (2 locations)
|
||||||
|
6. **CI/CD**: Add env vars to `.github/workflows/pr-tests.yml` and `release-pipeline.yml` (4 jobs)
|
||||||
|
7. **Docs**: Create `docs/providers/supported-providers/<name>.mdx`
|
||||||
|
8. **Test**: `make test-core PROVIDER=<name>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Always prefer `make test-core` over raw `go test` for provider-level tests
|
||||||
|
|
||||||
|
The `make test-core` target is the canonical harness for provider tests — it wires up env vars from `.env` (provider API keys), invokes the per-provider `{provider}_test.go` entrypoint in `core/providers/<provider>/`, and routes through the shared `core/internal/llmtests/` scenario suite that validates end-to-end behavior (including streaming).
|
||||||
|
|
||||||
|
Running bare `go test ./core/providers/<provider>/...` only executes unit tests and skips the llmtests scenarios — so it won't catch regressions in streaming, tool-calling, or provider-specific response shapes.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test-core PROVIDER=anthropic TESTCASE=TestChatCompletionStream # exact test
|
||||||
|
make test-core PROVIDER=openai PATTERN=Stream # substring match
|
||||||
|
make test-core PROVIDER=bedrock # all scenarios for one provider
|
||||||
|
make test-core DEBUG=1 PROVIDER=gemini TESTCASE=TestResponsesStream # attach Delve on :2345
|
||||||
|
```
|
||||||
|
|
||||||
|
`PATTERN` and `TESTCASE` are mutually exclusive. Provider name must match a directory under `core/providers/` (e.g. `anthropic`, `openai`, `bedrock`, `vertex`, `azure`, `gemini`, `cohere`, `mistral`, `groq`, etc.).
|
||||||
|
|
||||||
|
### LLM Tests (`core/internal/llmtests/`)
|
||||||
|
|
||||||
|
Scenario-based tests that run against **live provider APIs** with dual-API testing (Chat Completions + Responses API):
|
||||||
|
|
||||||
|
```go
|
||||||
|
func RunMyScenarioTest(t *testing.T, client *bifrost.Bifrost, ctx context.Context, cfg ComprehensiveTestConfig) {
|
||||||
|
// Use validation presets: BasicChatExpectations(), ToolCallExpectations(), etc.
|
||||||
|
// Use retry framework for flaky assertions
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Register in `tests.go` `testScenarios` slice
|
||||||
|
- Add `Scenarios.MyScenario` flag to `ComprehensiveTestConfig`
|
||||||
|
- Run: `make test-core PROVIDER=<name> TESTCASE=<TestName>`
|
||||||
|
|
||||||
|
### MCP Tests (`core/internal/mcptests/`)
|
||||||
|
|
||||||
|
Mock-based tests with `DynamicLLMMocker` and declarative setup:
|
||||||
|
|
||||||
|
```go
|
||||||
|
manager, mocker, ctx := SetupAgentTest(t, AgentTestConfig{
|
||||||
|
InProcessTools: []string{"echo", "calculator"},
|
||||||
|
AutoExecuteTools: []string{"*"},
|
||||||
|
MaxDepth: 5,
|
||||||
|
})
|
||||||
|
// Queue mock LLM responses, assert tool execution order
|
||||||
|
```
|
||||||
|
|
||||||
|
Categories: `agent_*_test.go`, `tool_*_test.go`, `connection_*_test.go`, `codemode_*_test.go`
|
||||||
|
|
||||||
|
Run: `make test-mcp TESTCASE=<TestName>`
|
||||||
|
|
||||||
|
### E2E Tests (`tests/e2e/`)
|
||||||
|
|
||||||
|
Playwright tests with page objects, data factories, fixtures:
|
||||||
|
|
||||||
|
- Page objects extend `BasePage`, use `getByTestId()` as primary selector strategy
|
||||||
|
- Data factories use `Date.now()` for unique names (prevents collision in parallel runs)
|
||||||
|
- Track created resources in arrays, clean up in `afterEach`
|
||||||
|
- Import `test`/`expect` from `../../core/fixtures/base.fixture` (never from `@playwright/test`)
|
||||||
|
- **Never marshal API payloads to a `Record`/`Map`/plain-object and then re-serialize.** Field ordering matters for snapshot comparisons and some backend validations. Construct payloads as object literals with fields in the intended order and pass directly to Playwright's `request.post({ data })`. Do NOT destructure into an intermediate `Record<string, unknown>` or use `Object.fromEntries()` / `JSON.parse(JSON.stringify(...))` round-trips, as these can reorder fields.
|
||||||
|
|
||||||
|
Run: `make run-e2e FLOW=<feature>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Claude Code Skills
|
||||||
|
|
||||||
|
Four skills are available via `/skill-name`:
|
||||||
|
|
||||||
|
### `/docs-writer <feature-name>`
|
||||||
|
Write, update, or review Mintlify MDX documentation. Researches UI code, Go handlers, and config schema. Validates `config.json` examples against `transports/config.schema.json`. Outputs docs with Web UI / API / config.json tabs.
|
||||||
|
|
||||||
|
Variants: `/docs-writer update <doc-path>`, `/docs-writer review <doc-path>`
|
||||||
|
|
||||||
|
### `/e2e-test <feature-name>`
|
||||||
|
Create, run, debug, audit, or auto-update Playwright E2E tests.
|
||||||
|
|
||||||
|
Variants:
|
||||||
|
- `/e2e-test fix <spec>` — Debug and fix a failing test
|
||||||
|
- `/e2e-test sync` — Detect UI changes, update affected tests automatically
|
||||||
|
- `/e2e-test audit` — Scan specs for incorrect/weak assertions (P0-P6 severity scale)
|
||||||
|
|
||||||
|
### `/investigate-issue <issue-id>`
|
||||||
|
Investigate a GitHub issue from `maximhq/bifrost`. Fetches issue details, classifies by type/area, searches codebase, traces dependencies, analyzes side effects, suggests tests (LLM/MCP/E2E), and presents an implementation plan with per-change approval gates.
|
||||||
|
|
||||||
|
### `/resolve-pr-comments <pr-number>`
|
||||||
|
Systematically address unresolved PR review comments. Uses GraphQL to get unresolved threads, presents each with FIX/REPLY/SKIP options, collects fixes locally, and only posts replies **after code is pushed** to remote.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Workflows
|
||||||
|
|
||||||
|
### Modify chat completions across all providers
|
||||||
|
1. Change types in `core/schemas/chatcompletions.go`
|
||||||
|
2. Update converter functions in each provider's `chat.go`
|
||||||
|
3. If streaming affected, update `framework/streaming/` (accumulator, delta copy)
|
||||||
|
4. Run `make test-core` (all providers)
|
||||||
|
|
||||||
|
### Add a new field to API responses
|
||||||
|
1. Add to schema type in `core/schemas/`
|
||||||
|
2. Map in provider response converter (`ToBifrost*Response`)
|
||||||
|
3. Handle in streaming accumulator if applicable
|
||||||
|
4. Update HTTP handler if field needs special serialization
|
||||||
|
5. Update `transports/config.schema.json` if configurable
|
||||||
|
|
||||||
|
### Add a new plugin
|
||||||
|
1. Create `plugins/<name>/` with its own `go.mod`
|
||||||
|
2. Implement `LLMPlugin`, `MCPPlugin`, or `HTTPTransportPlugin` interface
|
||||||
|
3. Add to `go.work`
|
||||||
|
4. Register in transport layer or Bifrost config
|
||||||
|
5. Add test targets to `Makefile`
|
||||||
|
|
||||||
|
### Modify a UI feature
|
||||||
|
1. Find workspace page: `ui/app/workspace/<feature>/`
|
||||||
|
2. Check existing `data-testid` attributes — E2E tests depend on them
|
||||||
|
3. Add `data-testid` to new interactive elements
|
||||||
|
4. Run `make run-e2e FLOW=<feature>` to verify
|
||||||
|
5. If E2E tests break, use `/e2e-test sync` to update them
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Files Quick Reference
|
||||||
|
|
||||||
|
| What | Where |
|
||||||
|
|------|-------|
|
||||||
|
| Main Bifrost struct & queuing | `core/bifrost.go` |
|
||||||
|
| Inference routing & fallbacks | `core/inference.go` |
|
||||||
|
| Provider interface (30+ methods) | `core/schemas/provider.go` |
|
||||||
|
| ModelProvider enum & context keys | `core/schemas/bifrost.go` |
|
||||||
|
| Plugin interfaces & pooled HTTP types | `core/schemas/plugin.go` |
|
||||||
|
| BifrostContext (mutable context) | `core/schemas/context.go` |
|
||||||
|
| Chat completion types | `core/schemas/chatcompletions.go` |
|
||||||
|
| Responses API types | `core/schemas/responses.go` |
|
||||||
|
| Object pool (prod + debug) | `core/pool/pool_prod.go`, `pool_debug.go` |
|
||||||
|
| Shared provider utils & SSE parsing | `core/providers/utils/utils.go` |
|
||||||
|
| Streaming accumulator | `framework/streaming/accumulator.go` |
|
||||||
|
| HTTP inference handler | `transports/bifrost-http/handlers/inference.go` |
|
||||||
|
| Governance handler | `transports/bifrost-http/handlers/governance.go` |
|
||||||
|
| Config schema (source of truth) | `transports/config.schema.json` |
|
||||||
|
| Pool debug profiler | `transports/bifrost-http/handlers/devpprof.go` |
|
||||||
|
| LLM test infrastructure | `core/internal/llmtests/` |
|
||||||
|
| MCP test infrastructure | `core/internal/mcptests/` |
|
||||||
|
| E2E test infrastructure | `tests/e2e/core/` |
|
||||||
|
| Docs navigation config | `docs/docs.json` |
|
||||||
|
| CI/CD workflows | `.github/workflows/` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- **Go**: `gofmt`/`goimports`. No custom linter config.
|
||||||
|
- **TypeScript/React**: Oxfmt. TanStack Router.
|
||||||
|
- **JSON tags**: `snake_case` matching provider API conventions.
|
||||||
|
- **Error strings**: Lowercase, no trailing punctuation (Go convention).
|
||||||
|
- **Provider types**: Prefixed with provider name in PascalCase (`AnthropicChatRequest`, `GeminiEmbeddingResponse`).
|
||||||
|
- **Converter functions**: Pure — no side effects, no logging, no HTTP.
|
||||||
|
- **Pool names**: Descriptive string passed to `pool.New()` (e.g., `"channel-message"`, `"response-stream"`).
|
||||||
|
- **Context keys**: Use `BifrostContextKey` type. Custom plugins should define their own key types to avoid collisions.
|
||||||
|
- **Go filenames**: No underscores. The only permitted underscore is the `_test.go` suffix. Examples: `pluginpipeline.go`, `pluginpipeline_test.go` — never `plugin_pipeline.go` or `plugin_pipeline_race_test.go`. Concatenate words (lowercase, no separators) for multi-word filenames.
|
||||||
|
|
||||||
|
# Frontend Code Guidelines & Patterns
|
||||||
|
|
||||||
|
This document defines the standards, structure, and best practices for writing frontend code in this project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **React** (with Vite)
|
||||||
|
- **TypeScript**
|
||||||
|
- **@tanstack/react-router** (type-safe routing)
|
||||||
|
- **Tailwind CSS v4**
|
||||||
|
- **Radix UI** (primitives)
|
||||||
|
- **Local UI component library** (`ui/components/ui/`) built on Radix primitives
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Folder Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
|
||||||
|
/ui
|
||||||
|
├── app # Routes & pages
|
||||||
|
├── components # Shared components
|
||||||
|
│ └── ui # Core design system components
|
||||||
|
├── hooks # Custom React hooks
|
||||||
|
├── lib # Utilities, helpers, shared logic
|
||||||
|
└── app/enterprise # Enterprise-specific code (via symlink)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
- All frontend code must live inside `/ui`
|
||||||
|
- Routes and pages → `ui/app`
|
||||||
|
- Shared/reusable components → `ui/components`
|
||||||
|
- Core UI primitives → `ui/components/ui`
|
||||||
|
- Utilities and libraries → `ui/lib`
|
||||||
|
- Custom hooks → `ui/hooks`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Libraries & Usage
|
||||||
|
|
||||||
|
### Core Libraries
|
||||||
|
|
||||||
|
- `react` → UI library
|
||||||
|
- `typescript` → Type safety
|
||||||
|
- `tailwindcss` → Styling
|
||||||
|
- `@tanstack/react-router` → Routing
|
||||||
|
|
||||||
|
### UI & Visualization
|
||||||
|
|
||||||
|
- `@radix-ui/react-*` → UI primitives
|
||||||
|
- `ui/components/ui/*` → Project's Radix-based component system
|
||||||
|
- `recharts` → Charts
|
||||||
|
- `monaco-editor` → Code editor
|
||||||
|
|
||||||
|
### Utilities
|
||||||
|
|
||||||
|
- `date-fns` → Date/time formatting
|
||||||
|
- `nuqs` → Query param state management
|
||||||
|
|
||||||
|
### Tooling
|
||||||
|
|
||||||
|
- `Oxfmt` → Code formatting
|
||||||
|
- `vitest` → Testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Routing Convention
|
||||||
|
|
||||||
|
For every new route:
|
||||||
|
|
||||||
|
```text
|
||||||
|
|
||||||
|
ui/app/<route-name>/
|
||||||
|
├── layout.tsx # Route definition using createFileRoute
|
||||||
|
├── page.tsx # Page content
|
||||||
|
└── views/ # Optional: route-specific components
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
- Folder name must match route name
|
||||||
|
- Always use `createFileRoute` in `layout.tsx`
|
||||||
|
- `page.tsx` should only handle composition (not heavy logic)
|
||||||
|
- Route-specific components go inside `views/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Component Guidelines
|
||||||
|
|
||||||
|
### Reusability First
|
||||||
|
|
||||||
|
- Always check if similar components/functions already exist
|
||||||
|
- Prefer extending or refactoring existing code over duplication
|
||||||
|
- Only create new components if reuse is not feasible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Component Placement
|
||||||
|
|
||||||
|
- Shared → `ui/components`
|
||||||
|
- Route-specific → `views/` inside route folder
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### JSX & Rendering
|
||||||
|
|
||||||
|
- Avoid deeply nested conditional rendering
|
||||||
|
- Break complex UI into smaller components
|
||||||
|
- Keep components readable and maintainable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Lists & Keys
|
||||||
|
|
||||||
|
- Always use **stable, unique keys**
|
||||||
|
- Never use array index as key (unless unavoidable)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## React Best Practices
|
||||||
|
|
||||||
|
- Avoid unnecessary or unstable dependencies in hooks
|
||||||
|
- Prevent infinite loops in `useEffect`
|
||||||
|
- Keep dependency arrays accurate and minimal
|
||||||
|
- Prefer derived state over duplicated state
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
|
||||||
|
### Priority Order
|
||||||
|
|
||||||
|
1. Query Params (`nuqs`) → for persistent/shareable state
|
||||||
|
2. Local State → for UI-only state
|
||||||
|
3. Redux → only when truly necessary
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Query Params (`nuqs`)
|
||||||
|
|
||||||
|
- Use for state that should persist across refresh/navigation
|
||||||
|
- Use proper parsers like `parseAsString` or `parseAsInteger`
|
||||||
|
- Do NOT mix query param state with local/redux state
|
||||||
|
- Follow a single consistent pattern across the codebase
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Redux
|
||||||
|
|
||||||
|
- Use only when global/shared state is required
|
||||||
|
- Avoid unnecessary slices
|
||||||
|
- Prefer simpler alternatives when possible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### RTK Query (`@reduxjs/toolkit/query`)
|
||||||
|
|
||||||
|
- Use for API calls and caching
|
||||||
|
- Use **granular tags** for cache invalidation
|
||||||
|
- Avoid invalidating entire datasets unnecessarily
|
||||||
|
- Implement **optimistic updates** where applicable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Forms
|
||||||
|
|
||||||
|
We use:
|
||||||
|
|
||||||
|
- `react-hook-form`
|
||||||
|
- `zod v4` (for schema validation)
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
- Always define a Zod schema
|
||||||
|
- Include meaningful validation messages
|
||||||
|
- Prefer **inline field errors** (not toast notifications)
|
||||||
|
- Use `refine` / `superRefine` for complex validation
|
||||||
|
- Store schemas in: `ui/lib/types/schemas.ts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tables
|
||||||
|
|
||||||
|
- Use `@tanstack/react-table` **only for large/complex datasets**
|
||||||
|
- For simple tables → build custom lightweight components
|
||||||
|
- Prioritize performance over abstraction
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Performance Guidelines
|
||||||
|
|
||||||
|
- Lazy load heavy or rarely-used libraries
|
||||||
|
- Avoid unnecessary re-renders
|
||||||
|
- Split large components into smaller ones
|
||||||
|
- Keep bundle size minimal
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependency Rules
|
||||||
|
|
||||||
|
- Do NOT add new dependencies unless absolutely necessary
|
||||||
|
- Always pin exact versions (no `^` or `~`)
|
||||||
|
- Prefer existing libraries in the codebase
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TypeScript Guidelines
|
||||||
|
|
||||||
|
- Avoid using `any` unless absolutely unavoidable
|
||||||
|
- Prefer strict typing and inference
|
||||||
|
- Define reusable types in shared locations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Quality & Formatting
|
||||||
|
|
||||||
|
After writing code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ui && npm run format
|
||||||
|
````
|
||||||
|
|
||||||
|
Then verify build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ui && npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
* Code must pass formatting and build checks
|
||||||
|
* Follow consistent naming and structure conventions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anti-Patterns to Avoid
|
||||||
|
|
||||||
|
* Duplicate components without considering reuse
|
||||||
|
* Mixing multiple state management approaches unnecessarily
|
||||||
|
* Overusing Redux
|
||||||
|
* Using unstable hook dependencies
|
||||||
|
* Adding heavy libraries for simple use cases
|
||||||
|
* Poorly structured or deeply nested JSX
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
* Prioritize **reusability, performance, and consistency**
|
||||||
|
* Follow **strict folder structure and routing conventions**
|
||||||
|
* Use **the right tool for the right problem**
|
||||||
|
* Keep code **simple, predictable, and maintainable**
|
||||||
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
akshay@getmaxim.ai.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user