name: Release Pipeline # Triggers automatically on push to main when any version file changes on: push: branches: "main" workflow_dispatch: inputs: skip_tests: description: "Skip all tests and jump directly to release (admin only)" required: false type: boolean default: false # Prevent concurrent runs concurrency: group: release-pipeline cancel-in-progress: false permissions: contents: read 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 }} skip-core-test: ${{ steps.check.outputs.skip-core-test }} skip-tests: ${{ steps.skip_tests.outputs.skip-tests }} steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: >+ api.github.com:443 - name: Check if pipeline should be skipped id: check env: COMMIT_MESSAGE: ${{ github.event.head_commit.message }} run: | # head_commit is only populated on push. On workflow_dispatch this is # empty, so both flags read false there - the dispatch equivalent is the # admin-gated skip_tests input below. 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 # --skip-core-test drops the three jobs that make live calls to every # upstream provider: test-core (the ~90 minute provider harness), # test-core-integrations (the core module's live provider suite) and # test-proxy-e2e (the harness smoke set through real proxies). Every # other test job still runs, including test-core-unit, so the release is # still gated on the whole suite minus the jobs that depend on eight # third-party APIs being healthy at that moment. # # Skipping them does not cascade: every downstream release job already # accepts `needs..result == 'skipped'` alongside 'success' (the # same path a bifrost-http-only release takes when test-core never runs). # # Distinct from --skip-ci, which skips the entire pipeline including the # release, and from skip_tests, which drops every test job and is admin-only. # This one is deliberately NOT permission-gated: it still ships a release # whose unit, integration, migration, docker and API-integration coverage all # passed. Use it when a provider outage is blocking a release that has # nothing to do with that provider - not to get a red harness out the door. if [[ "$FIRST_LINE" == *"--skip-core-test"* ]]; then echo "::warning::--skip-core-test found in the commit subject: the live provider jobs (test-core, test-core-integrations, test-proxy-e2e) will not run for this release. All other test jobs still gate it." echo "skip-core-test=true" >> $GITHUB_OUTPUT else echo "skip-core-test=false" >> $GITHUB_OUTPUT fi - name: Validate admin permission for skip_tests id: skip_tests env: GH_TOKEN: ${{ secrets.GH_TOKEN }} run: | if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.skip_tests }}" == "true" ]]; then PERMISSION=$(gh api repos/${{ github.repository }}/collaborators/${{ github.actor }}/permission --jq '.permission') if [[ "$PERMISSION" != "admin" ]]; then echo "::error::Only repository admins can use skip_tests=true. ${{ github.actor }} has permission: $PERMISSION" exit 1 fi echo "Admin verified: ${{ github.actor }} ($PERMISSION)" echo "skip-tests=true" >> $GITHUB_OUTPUT else echo "skip-tests=false" >> $GITHUB_OUTPUT fi # Phone gate. Parks the run on the release-run-approval environment, whose only # protection rule is the bifrost-release-gate GitHub App. The service behind that # App pushes an Approve/Reject prompt to the admin's phone and calls back GitHub # with the answer. Nothing below runs - no provider quota, no runner minutes - # until the tap arrives. Version-bump pushes carry --skip-ci and never reach this # job. The job holds no secrets, checks out nothing and allows no egress: it is a # named point in the graph for the environment rule to hold, nothing more. approve-run: needs: [check-skip] if: needs.check-skip.outputs.should-skip != 'true' runs-on: ubuntu-latest environment: name: release-run-approval url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} permissions: {} steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: >+ - name: Record the approved run run: echo "Run ${GITHUB_RUN_ID} for ${GITHUB_SHA} was approved from the phone gate" # Detect what needs to be released detect-changes: needs: [check-skip, approve-run] runs-on: ubuntu-latest # Skip if first line of commit message contains --skip-ci, or if the phone # gate above was rejected. Every other job needs this one, so this edge is # what holds the whole graph behind the gate. if: needs.check-skip.outputs.should-skip != 'true' && needs.approve-run.result == 'success' outputs: core-needs-release: ${{ steps.detect.outputs.core-needs-release }} framework-needs-release: ${{ steps.detect.outputs.framework-needs-release }} plugins-need-release: ${{ steps.detect.outputs.plugins-need-release }} bifrost-http-needs-release: ${{ steps.detect.outputs.bifrost-http-needs-release }} docker-needs-release: ${{ steps.detect.outputs.docker-needs-release }} changed-plugins: ${{ steps.detect.outputs.changed-plugins }} core-version: ${{ steps.detect.outputs.core-version }} framework-version: ${{ steps.detect.outputs.framework-version }} transport-version: ${{ steps.detect.outputs.transport-version }} steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > _http._tcp.azure.archive.ubuntu.com:443 _https._tcp.esm.ubuntu.com:443 _https._tcp.motd.ubuntu.com:443 _https._tcp.packages.microsoft.com:443 _https._tcp.dl.google.com:443 motd.ubuntu.com:443 dl.google.com:443 api.github.com:443 azure.archive.ubuntu.com:80 dl.google.com:443 esm.ubuntu.com:443 github.com:443 packages.microsoft.com:443 proxy.golang.org:443 registry.hub.docker.com:443 release-assets.githubusercontent.com:443 storage.googleapis.com:443 sum.golang.org:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 fetch-tags: true - name: Install jq run: | sudo apt-get update sudo apt-get install -y jq - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.27.3" - name: Detect what needs release id: detect run: ./.github/workflows/scripts/detect-all-changes.sh "auto" - 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 # Builds the UI and the bifrost-http binary ONCE, and publishes the binary as # an artifact for every test job that just needs a runnable gateway. # # Before this job existed, nine jobs each built it themselves. Measured on a # cli-harness leg: `make build-ui` ~54s, `go build` on bifrost-http ~2m30s - # so ~3.5 minutes x 9 jobs, ~30 runner-minutes of duplicated work per run. # Consumers now download this artifact and pass SKIP_GATEWAY_BUILD=1, which # every build helper honours (harness-gateway.sh, test-api-integrations.sh, # cost-accuracy-test.sh, run-migration-tests.sh; load-test.sh already skips # when tmp/bifrost-http is present). # # test-core deliberately does NOT depend on this job. It is the pipeline's # longest job (~54m) and therefore its critical path, so making it wait for a # build it can do concurrently would push the whole run out by ~4 minutes. # It keeps its inline harness_build_gateway call. # # Gating is the union of the consumers' own conditions - if it were narrower, # a consumer could run with no artifact to download and fail at the download # step rather than falling back to building. build-gateway: needs: [check-skip, detect-changes] if: | needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && ( needs.detect-changes.outputs.core-needs-release == 'true' || needs.detect-changes.outputs.framework-needs-release == 'true' || needs.detect-changes.outputs.plugins-need-release == 'true' || needs.detect-changes.outputs.bifrost-http-needs-release == 'true' || needs.detect-changes.outputs.docker-needs-release == 'true' ) runs-on: ubuntu-latest timeout-minutes: 30 permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block # Build-only: the Go module proxy, the npm registry and the Node/Go # toolchain downloads. No provider hosts - nothing is executed here. allowed-endpoints: > api.github.com:443 azure.archive.ubuntu.com:80 esm.ubuntu.com:443 fonts.googleapis.com:443 fonts.gstatic.com:443 github.com:443 iojs.org:443 nodejs.org:443 objects.githubusercontent.com:443 proxy.golang.org:443 registry.npmjs.org:443 release-assets.githubusercontent.com:443 storage.googleapis.com:443 sum.golang.org:443 - 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.27.3" - name: Set up Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "25" - name: Build UI and bifrost-http binary run: | # The workspace must exist before the build so the transport resolves # core/framework/plugins from the checkout rather than the proxy. source ./.github/workflows/scripts/setup-go-workspace.sh echo "šŸŽØ Building UI..." make build-ui echo "šŸ”Ø Building bifrost-http binary..." mkdir -p tmp (cd transports/bifrost-http && go build -o ../../tmp/bifrost-http .) ls -lh tmp/bifrost-http - name: Upload bifrost-http binary uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: bifrost-http-binary path: tmp/bifrost-http # Consumed within the same run only; no reason to keep it around. retention-days: 1 if-no-files-found: error # A ~100MB Go binary compresses poorly, and the CPU spent on level 6 # costs more than the bytes it saves on the intra-run round trip. compression-level: 1 # Run all tests in parallel before any releases # # test-core no longer runs the core module's `go test` suite. It validates the # core build, then runs the provider harness (tests/e2e/api/collections/provider-harness.json) # end-to-end against a locally built bifrost-http binary - see # .github/workflows/scripts/test-provider-harness.sh. The harness runs on the # host and talks to real provider APIs, so every provider endpoint the # collection touches must be allowed below. test-core: needs: [check-skip, detect-changes] # Gated on core alone. The provider harness only exercises core's provider # code paths, so a release that does not ship a new core has nothing here to # validate and should not pay the ~90 minute critical path for it. # # This deliberately means a bifrost-http-only release runs no provider # harness. Every downstream release job already accepts # `needs.test-core.result == 'skipped'`, so the release still proceeds; the # transport keeps its own coverage through test-bifrost-http, # test-api-integrations and test-migrations. Matches test-cli-harness. if: | needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && needs.check-skip.outputs.skip-core-test != 'true' && needs.detect-changes.outputs.core-needs-release == 'true' runs-on: ubuntu-latest timeout-minutes: 90 permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block # 127.0.0.1:8080 is the bifrost-http binary the harness drives. # getbifrost.ai / www.getbifrost.ai serve the pricing datasheet the # dbverify newman reporter checks logged costs against. # dl.google.com / packages.cloud.google.com / objects.githubusercontent.com # are the gcloud CLI install; oauth2.googleapis.com + accounts.google.com # are the service-account token exchange for the Vertex parity cells. # # www.berkshirehathaway.com hosts the PDF the document-input rows pass as # document.source.type "url". Anthropic fetches such a URL server-side, but # Bedrock/Vertex/Gemini/OpenAI/Azure have no url member on their document # types, so Bifrost downloads it here and inlines the bytes - meaning the # RUNNER needs egress to it. Omitting it fails 44 rows whose requests all # go to 127.0.0.1, which reads as "PDF input is broken on five providers". # # discoveryengine.googleapis.com is the Vertex semantic-ranker backend for # the rerank rows. Its host is built in Go rather than declared in # config.json, so auditing the seeded provider config does not reveal it. allowed-endpoints: > 127.0.0.1:8080 7defe2860d5ee49a1e667e1eeea34b25.r2.cloudflarestorage.com:443 accounts.google.com:443 aiplatform.googleapis.com:443 api.anthropic.com:443 api.cerebras.ai:443 api.minimax.io:443 ark.ap-southeast.bytepluses.com:443 api.cloudflare.com:443 api.cohere.ai:443 api.deepseek.com:443 api.elevenlabs.io:443 api.fireworks.ai:443 api.github.com:443 api.githubcopilot.com:443 api.business.githubcopilot.com:443 api.enterprise.githubcopilot.com:443 api.individual.githubcopilot.com:443 api.groq.com:443 api.mistral.ai:443 api.openai.com:443 api.parasail.io:443 api.perplexity.ai:443 api.replicate.com:443 api.tokenfactory.nebius.com:443 api.x.ai:443 azure.archive.ubuntu.com:80 esm.ubuntu.com:443 bifrost-gateway-resource.openai.azure.com:443 bedrock-mantle.us-east-1.api.aws:443 bedrock-runtime.us-east-1.amazonaws.com:443 bedrock.us-east-1.amazonaws.com:443 bifrost-batch-api-file-upload-testing.s3.us-east-1.amazonaws.com:443 discoveryengine.googleapis.com:443 dl.google.com:443 fal.media:443 fonts.googleapis.com:443 fonts.gstatic.com:443 generativelanguage.googleapis.com:443 files.pythonhosted.org:443 pypi.org:443 raw.githubusercontent.com:443 getbifrost.ai:443 github.com:443 huggingface.co:443 iojs.org:443 login.microsoftonline.com:443 maxim-o1.openai.azure.com:443 nodejs.org:443 oauth2.googleapis.com:443 objects.githubusercontent.com:443 openrouter.ai:443 packages.cloud.google.com:443 proxy.golang.org:443 registry.npmjs.org:443 release-assets.githubusercontent.com:443 router.huggingface.co:443 storage.googleapis.com:443 sum.golang.org:443 us-central1-aiplatform.googleapis.com:443 www.berkshirehathaway.com:443 www.getbifrost.ai:443 api.dev.runwayml.com:443 api.sarvam.ai:443 api.typesafe.ai:443 - 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.27.3" - name: Set up Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "25" - name: Install jq run: | sudo apt-get update sudo apt-get install -y jq # npm ci against .github/workflows/tools/package-lock.json, not `npm install -g`: # the lockfile carries an integrity hash for newman and every transitive dep, which # a bare version pin does not. Bin dir goes on PATH because the test scripts probe # for newman with `command -v`. - name: Install Newman (hash-pinned via lockfile) run: | npm ci --no-audit --no-fund echo "$PWD/node_modules/.bin" >> "$GITHUB_PATH" working-directory: .github/workflows/tools - name: Set up gcloud CLI uses: step-security/setup-gcloud@0d4fd148182bcc22d7b88f0e12ccc0150ab691f4 # v3.0.2 with: version: "575.0.1" - name: Run core tests env: 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 }} AZURE_API_VERSION: ${{ secrets.AZURE_API_VERSION }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} 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 }} ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }} PERPLEXITY_API_KEY: ${{ secrets.PERPLEXITY_API_KEY }} SGL_API_KEY: ${{ secrets.SGL_API_KEY }} CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }} MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }} DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} SARVAM_API_KEY: ${{ secrets.SARVAM_API_KEY }} TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }} DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} DATABRICKS_WORKSPACE_URL: ${{ secrets.DATABRICKS_WORKSPACE_URL }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} GITHUB_COPILOT_API_KEY: ${{ secrets.GITHUB_COPILOT_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 }} VERTEX_GCS_BUCKET: ${{ secrets.VERTEX_GCS_BUCKET }} VERTEX_GCS_PREFIX: ${{ secrets.VERTEX_GCS_PREFIX }} HUGGING_FACE_API_KEY: ${{ secrets.HUGGING_FACE_API_KEY }} AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} AWS_BEDROCK_ROLE_ARN: ${{ secrets.AWS_BEDROCK_ROLE_ARN }} BIFROST_ENCRYPTION_KEY: ${{ secrets.BIFROST_ENCRYPTION_KEY }} # Additional vars the provider harness forwards to newman: # AWS_REGION + GOOGLE_LOCATION drive the direct-provider token-parity # calls, the rest back providers exercised by the criss-cross matrix. AWS_REGION: ${{ secrets.AWS_REGION }} GOOGLE_LOCATION: ${{ secrets.GOOGLE_LOCATION }} XAI_API_KEY: ${{ secrets.XAI_API_KEY }} REPLICATE_API_KEY: ${{ secrets.REPLICATE_API_KEY }} BEDROCK_GUARDRAIL_IDENTIFIER: ${{ secrets.BEDROCK_GUARDRAIL_IDENTIFIER }} # Deliberately a variable, not a secret. Guardrail versions are short # integers, and Actions masks a secret's value everywhere in the job # log with no word boundary - a value of "1" redacts every "1" the # harness prints, turning "fail 141" into "fail ***4***". Not # sensitive, so vars.* (never auto-masked) is the right home. BEDROCK_GUARDRAIL_VERSION: ${{ vars.BEDROCK_GUARDRAIL_VERSION }} NEBIUS_API_KEY: ${{ secrets.NEBIUS_API_KEY }} RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }} run: ./.github/workflows/scripts/test-core.sh - name: Upload provider harness reports if: ${{ !cancelled() }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: provider-harness-reports path: | tmp/newman-report*.json tmp/newman-cli*.log tmp/provider-harness-report.html tmp/harness-failures.md tmp/harness-token-parity.md tmp/stream-cancel-report.json tmp/bifrost-dev.log # Per-attempt snapshots, written when a run needed RERUN_FAILED # retries. The files above only reflect the LAST attempt, so this is # the only record of what the first attempt actually failed on. tmp/harness-attempts/ retention-days: 30 # Publishes the provider harness's failure breakdown to R2 for the release # notes. Note this is markdown, not HTML: the CI run is PARALLEL=1, and the # htmlextra report is only produced in sequential mode, so there is no HTML # report to publish here. newman-report.json is deliberately excluded - it # routinely exceeds 500MB (see tests/e2e/api/runners/lib/read-report.mjs). - name: Publish provider harness report if: ${{ !cancelled() }} env: 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: | chmod +x ./.github/workflows/scripts/configure-r2.sh ./.github/workflows/scripts/upload-test-reports-to-r2.sh ./.github/workflows/scripts/configure-r2.sh || { echo "::warning::R2 not configured; skipping report publish"; exit 0; } mkdir -p tmp/provider-harness-report # index.html so the R2 key serves as a browsable page, matching the # cli-harness reports the changelog links alongside it. cp tmp/provider-harness-report.html tmp/provider-harness-report/index.html 2>/dev/null || true cp tmp/harness-failures.md tmp/harness-token-parity.md tmp/provider-harness-report/ 2>/dev/null || true ./.github/workflows/scripts/upload-test-reports-to-r2.sh \ "${{ needs.detect-changes.outputs.transport-version }}" \ "provider-harness" \ tmp/provider-harness-report # Real provider calls through real proxies, for every way Bifrost can be told to use # one: squid (http://), stunnel in front of squid (https://, TLS to the proxy) and # microsocks (socks5://), all requiring credentials, crossed with the global proxy, # per-provider proxy_config and proxy_config type environment + HTTPS_PROXY. See # tests/e2e/api/runners/proxy/run-proxy-matrix.sh. # # Each cell runs proxy-smoke-manifest.json (one upstream-reaching row per provider) # and then requires every provider host in that cell's proxy log. Strict egress # confines the gateway to loopback with iptables, so a request that skips the proxy # fails rather than quietly going direct - the failure mode a customer behind a # corporate proxy hits as TLS errors or timeouts. # # Gated like test-core, and extended to framework and bifrost-http releases, since # the outbound clients (OAuth, MCP, catalog sync, webhooks) and the global proxy # wiring live there. Every release job below needs it green or skipped. test-proxy-e2e: needs: [check-skip, detect-changes, build-gateway] if: | needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && needs.check-skip.outputs.skip-core-test != 'true' && ( needs.detect-changes.outputs.core-needs-release == 'true' || needs.detect-changes.outputs.framework-needs-release == 'true' || needs.detect-changes.outputs.bifrost-http-needs-release == 'true' ) runs-on: ubuntu-latest timeout-minutes: 45 permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block # test-core's allowlist: the proxies run on this runner, so every provider # host still leaves it. Plus the runware, runway and typesafe upstreams, # which the manifest reaches and test-core's list does not name, and every # other host the harness sources name, which check-egress-allowlist.sh # requires of a harness job (the .invalid and example names never resolve). allowed-endpoints: > 127.0.0.1:8080 7defe2860d5ee49a1e667e1eeea34b25.r2.cloudflarestorage.com:443 accounts.google.com:443 aiplatform.googleapis.com:443 api.anthropic.com:443 api.cerebras.ai:443 api.cohere.ai:443 api.deepseek.com:443 api.elevenlabs.io:443 api.fireworks.ai:443 api.github.com:443 api.githubcopilot.com:443 api.business.githubcopilot.com:443 api.enterprise.githubcopilot.com:443 api.individual.githubcopilot.com:443 api.groq.com:443 api.mistral.ai:443 api.openai.com:443 api.parasail.io:443 api.perplexity.ai:443 api.replicate.com:443 api.tokenfactory.nebius.com:443 api.x.ai:443 azure.archive.ubuntu.com:80 esm.ubuntu.com:443 bifrost-gateway-resource.openai.azure.com:443 bedrock-mantle.us-east-1.api.aws:443 bedrock-runtime.us-east-1.amazonaws.com:443 bedrock.us-east-1.amazonaws.com:443 bifrost-batch-api-file-upload-testing.s3.us-east-1.amazonaws.com:443 discoveryengine.googleapis.com:443 dl.google.com:443 fal.media:443 fonts.googleapis.com:443 fonts.gstatic.com:443 generativelanguage.googleapis.com:443 files.pythonhosted.org:443 pypi.org:443 getbifrost.ai:443 github.com:443 huggingface.co:443 iojs.org:443 login.microsoftonline.com:443 maxim-o1.openai.azure.com:443 nodejs.org:443 oauth2.googleapis.com:443 objects.githubusercontent.com:443 openrouter.ai:443 packages.cloud.google.com:443 proxy.golang.org:443 registry.npmjs.org:443 release-assets.githubusercontent.com:443 releases.astral.sh:443 router.huggingface.co:443 storage.googleapis.com:443 sum.golang.org:443 us-central1-aiplatform.googleapis.com:443 www.berkshirehathaway.com:443 www.getbifrost.ai:443 api.runware.ai:443 api.dev.runwayml.com:443 api.typesafe.ai:443 ai.google.dev:443 code.claude.com:443 dbx-fixture-workspace.invalid:443 does-not-exist-h146.search.windows.net:443 does-not-exist-h150.invalid:443 does-not-resolve.invalid:443 harness-credential-guard.example.invalid:443 harness-endpoint-guard.example.invalid:443 harness-lib-installed.invalid:443 harness-plugin-guard.example.invalid:443 iamcredentials.googleapis.com:443 idp.example:443 learn.microsoft.com:443 mcp.deepwiki.com:443 petstore3.swagger.io:443 other-mcp.invalid:443 auth.example.invalid:443 inspector.example.com:443 mcp.example.invalid:443 evil.example:443 replace-me.search.windows.net:443 sts.googleapis.com:443 unlisted.example:443 www.ietf.org:443 www.w3.org:443 www.wikipedia.org:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # The harness runs with provider secrets and nothing here uses git afterwards, # so the checkout token is not left behind for those steps. persist-credentials: false - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.27.3" - name: Set up Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "25" # The Ubuntu squid and microsocks packages start system services on the # ports the flow uses; stop them so the flow's own configured instances bind. - name: Install proxies and jq run: | sudo apt-get update sudo apt-get install -y jq squid stunnel4 microsocks sudo systemctl disable --now squid 2>/dev/null || true sudo systemctl disable --now microsocks 2>/dev/null || true - name: Install Newman (hash-pinned via lockfile) run: | npm ci --no-audit --no-fund echo "$PWD/node_modules/.bin" >> "$GITHUB_PATH" working-directory: .github/workflows/tools - name: Download prebuilt bifrost-http binary uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: bifrost-http-binary path: tmp - name: Make bifrost-http executable run: chmod +x tmp/bifrost-http - name: Run proxy e2e env: SKIP_GATEWAY_BUILD: "1" 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 }} AZURE_API_VERSION: ${{ secrets.AZURE_API_VERSION }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} 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 }} ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }} PERPLEXITY_API_KEY: ${{ secrets.PERPLEXITY_API_KEY }} SGL_API_KEY: ${{ secrets.SGL_API_KEY }} CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }} DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} SARVAM_API_KEY: ${{ secrets.SARVAM_API_KEY }} TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }} DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} DATABRICKS_WORKSPACE_URL: ${{ secrets.DATABRICKS_WORKSPACE_URL }} GITHUB_COPILOT_APP_ID: ${{ secrets.GITHUB_COPILOT_APP_ID }} GITHUB_COPILOT_INSTALLATION_ID: ${{ secrets.GITHUB_COPILOT_INSTALLATION_ID }} GITHUB_COPILOT_REPOSITORY_ID: ${{ secrets.GITHUB_COPILOT_REPOSITORY_ID }} GITHUB_COPILOT_PRIVATE_KEY: ${{ secrets.GITHUB_COPILOT_PRIVATE_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 }} VERTEX_GCS_BUCKET: ${{ secrets.VERTEX_GCS_BUCKET }} VERTEX_GCS_PREFIX: ${{ secrets.VERTEX_GCS_PREFIX }} HUGGING_FACE_API_KEY: ${{ secrets.HUGGING_FACE_API_KEY }} AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} AWS_BEDROCK_ROLE_ARN: ${{ secrets.AWS_BEDROCK_ROLE_ARN }} BIFROST_ENCRYPTION_KEY: ${{ secrets.BIFROST_ENCRYPTION_KEY }} # Additional vars the provider harness forwards to newman: # AWS_REGION + GOOGLE_LOCATION drive the direct-provider token-parity # calls, the rest back providers exercised by the criss-cross matrix. AWS_REGION: ${{ secrets.AWS_REGION }} GOOGLE_LOCATION: ${{ secrets.GOOGLE_LOCATION }} XAI_API_KEY: ${{ secrets.XAI_API_KEY }} REPLICATE_API_KEY: ${{ secrets.REPLICATE_API_KEY }} BEDROCK_GUARDRAIL_IDENTIFIER: ${{ secrets.BEDROCK_GUARDRAIL_IDENTIFIER }} # Deliberately a variable, not a secret. Guardrail versions are short # integers, and Actions masks a secret's value everywhere in the job # log with no word boundary - a value of "1" redacts every "1" the # harness prints, turning "fail 141" into "fail ***4***". Not # sensitive, so vars.* (never auto-masked) is the right home. BEDROCK_GUARDRAIL_VERSION: ${{ vars.BEDROCK_GUARDRAIL_VERSION }} NEBIUS_API_KEY: ${{ secrets.NEBIUS_API_KEY }} RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }} RUNWARE_API_KEY: ${{ secrets.RUNWARE_API_KEY }} run: ./.github/workflows/scripts/test-proxy-e2e.sh - name: Upload proxy e2e reports if: ${{ !cancelled() }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: proxy-e2e-reports path: tmp/proxy-e2e/cells/ retention-days: 30 test-core-integrations: needs: [check-skip, detect-changes, build-gateway] # skip-core-test drops this alongside test-core: both make live calls to the # upstream providers, so a provider outage that blocks one blocks both. if: needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && needs.check-skip.outputs.skip-core-test != 'true' && needs.detect-changes.outputs.core-needs-release == 'true' runs-on: ubuntu-latest timeout-minutes: 90 permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: &test-core-egress > 127.0.0.1:8080 7defe2860d5ee49a1e667e1eeea34b25.r2.cloudflarestorage.com:443 accounts.google.com:443 aiplatform.googleapis.com:443 api.anthropic.com:443 api.cerebras.ai:443 api.minimax.io:443 ark.ap-southeast.bytepluses.com:443 api.cloudflare.com:443 api.cohere.ai:443 api.deepseek.com:443 api.elevenlabs.io:443 api.fireworks.ai:443 api.github.com:443 api.githubcopilot.com:443 api.business.githubcopilot.com:443 api.enterprise.githubcopilot.com:443 api.individual.githubcopilot.com:443 api.groq.com:443 api.mistral.ai:443 api.openai.com:443 api.parasail.io:443 api.perplexity.ai:443 api.replicate.com:443 api.tokenfactory.nebius.com:443 api.x.ai:443 azure.archive.ubuntu.com:80 esm.ubuntu.com:443 bifrost-gateway-resource.openai.azure.com:443 bedrock-mantle.us-east-1.api.aws:443 bedrock-runtime.us-east-1.amazonaws.com:443 bedrock.us-east-1.amazonaws.com:443 bifrost-batch-api-file-upload-testing.s3.us-east-1.amazonaws.com:443 discoveryengine.googleapis.com:443 dl.google.com:443 fal.media:443 fonts.googleapis.com:443 fonts.gstatic.com:443 generativelanguage.googleapis.com:443 files.pythonhosted.org:443 pypi.org:443 getbifrost.ai:443 github.com:443 huggingface.co:443 iojs.org:443 login.microsoftonline.com:443 maxim-o1.openai.azure.com:443 nodejs.org:443 oauth2.googleapis.com:443 objects.githubusercontent.com:443 openrouter.ai:443 packages.cloud.google.com:443 proxy.golang.org:443 registry.npmjs.org:443 release-assets.githubusercontent.com:443 releases.astral.sh:443 router.huggingface.co:443 storage.googleapis.com:443 sum.golang.org:443 us-central1-aiplatform.googleapis.com:443 www.berkshirehathaway.com:443 www.getbifrost.ai:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.27.3" - name: Set up Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "25" # test-integrations.sh installs the Python suite with `uv sync --frozen`, and # silently degrades to `pip install -e .` when uv is absent. That fallback cannot # succeed here: tests/integrations/python is a dependency manifest, not a package, # so setuptools' flat-layout discovery aborts on the two top-level run_*.py modules. # Installing uv keeps the job on the locked, reproducible path. Pinned to the same # action and version snyk.yml already uses; python-version matches # tests/integrations/python/.python-version so CI resolves the interpreter a # developer gets locally instead of downloading a second one. - name: Install uv uses: step-security/setup-uv@ccf0a26ce9117d9e99292b0ce953ea5d9ffe778e # v7.3.0 with: version: "0.11.0" python-version: "3.12" - name: Download prebuilt bifrost-http binary uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: bifrost-http-binary path: tmp - name: Make bifrost-http executable run: chmod +x tmp/bifrost-http # Backs config.json's sse_mcp client, which test-integrations.sh starts on # localhost:3012. Replaces the decommissioned hosted endpoint that config # used to reference through MCP_SSE_URL. - name: Build MCP test servers run: make setup-mcp-tests - name: Run core integration test files in parallel env: SKIP_GATEWAY_BUILD: "1" # Ceiling on concurrently running test files. Set explicitly rather than # left to the script's default so the release gate's provider-quota budget # is visible and tunable here, next to the credentials it spends. INTEGRATION_TEST_MAX_PARALLEL: "4" 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_REGION: ${{ secrets.AWS_REGION }} AWS_ARN: ${{ secrets.AWS_ARN }} BEDROCK_API_KEY: ${{ secrets.BEDROCK_API_KEY }} AZURE_ENDPOINT: ${{ secrets.AZURE_ENDPOINT }} AZURE_API_KEY: ${{ secrets.AZURE_API_KEY }} AZURE_API_VERSION: ${{ secrets.AZURE_API_VERSION }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} 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 }} ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }} PERPLEXITY_API_KEY: ${{ secrets.PERPLEXITY_API_KEY }} SGL_API_KEY: ${{ secrets.SGL_API_KEY }} CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }} MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }} DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} SARVAM_API_KEY: ${{ secrets.SARVAM_API_KEY }} TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }} DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} DATABRICKS_WORKSPACE_URL: ${{ secrets.DATABRICKS_WORKSPACE_URL }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} GITHUB_COPILOT_API_KEY: ${{ secrets.GITHUB_COPILOT_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 }} VERTEX_GCS_BUCKET: ${{ secrets.VERTEX_GCS_BUCKET }} VERTEX_GCS_PREFIX: ${{ secrets.VERTEX_GCS_PREFIX }} GOOGLE_LOCATION: ${{ secrets.GOOGLE_LOCATION }} HUGGING_FACE_API_KEY: ${{ secrets.HUGGING_FACE_API_KEY }} XAI_API_KEY: ${{ secrets.XAI_API_KEY }} REPLICATE_API_KEY: ${{ secrets.REPLICATE_API_KEY }} NEBIUS_API_KEY: ${{ secrets.NEBIUS_API_KEY }} RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }} run: ./.github/workflows/scripts/test-integrations.sh --parallel-files test-core-unit: needs: [check-skip, detect-changes] if: needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && needs.detect-changes.outputs.core-needs-release == 'true' runs-on: ubuntu-latest permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: *test-core-egress - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.27.3" - name: Set up Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "25" # core/internal/mcptests drives real STDIO MCP servers as child processes. # fixtures.go's InitMCPServerPaths only computes their paths # (examples/mcps//bin/, .../dist/index.js) - nothing builds # them. Without this step every STDIO-backed case fails with "MCP client # ... is disconnected", and the agent permission tests fail one step # removed, counting an un-executed tool as still pending approval. - name: Build MCP test servers run: make setup-mcp-tests # No MCP_HTTP_URL / MCP_SSE_URL here on purpose. The remote-transport # cases boot examples/mcps/remote-test-server in-process (TestMain in # core/internal/mcptests/remoteserver_test.go) and point themselves at # localhost, so this job needs no secret and no extra egress entry. # Setting those vars here would do nothing - the local server wins unless # MCP_USE_REMOTE=1 is also set. - name: Run core unit tests working-directory: core run: | packages=() while IFS=$'\t' read -r package directory; do if [[ "$package" == */internal/llmtests ]]; then continue fi if find "$directory" -maxdepth 1 -name '*_test.go' -exec grep -lq '/internal/llmtests' {} +; then echo "Skipping llmtests-backed package: $package" continue fi packages+=("$package") done < <(go list -f '{{.ImportPath}}{{"\t"}}{{.Dir}}' ./...) if (( ${#packages[@]} == 0 )); then echo "::error::No core unit-test packages found" exit 1 fi go test -timeout 20m "${packages[@]}" # Break-glass for a test-core failure that survived its retries. # # test-core already re-runs its failed items twice via RERUN_FAILED (see # test-provider-harness.sh), so reaching this job means three attempts failed # - a flaky provider call has largely been ruled out by then. This exists for # the case retries cannot fix, e.g. a provider-side outage lasting the whole # run. Approving still releases on a red test-core, so prefer fixing forward. approve-flaky-test-core: needs: [check-skip, detect-changes, test-core] if: | always() && needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && needs.detect-changes.outputs.core-needs-release == 'true' && needs.test-core.result == 'failure' runs-on: ubuntu-latest environment: name: flaky-test-override url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} outputs: approved: ${{ steps.approve.outputs.approved }} steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: >+ - name: Display failed test info run: | echo "::warning::test-core failed after all RERUN_FAILED retries (3 attempts: 1 full sweep + 2 failed-item re-runs)." echo "A transient flake would normally have cleared on retry, so treat this as a real failure by default." echo "Check tmp/harness-attempts/ in the provider-harness-reports artifact: if the same items failed on" echo "every attempt, it is a regression - reject and fix. Approve only for something retries cannot fix," echo "such as a provider-side outage spanning the whole run." - name: Mark as approved id: approve run: echo "approved=true" >> $GITHUB_OUTPUT # Memory-regression gate. # # Two production memory bugs shipped in quick succession with opposite # signatures: a raw-JSON rewrite that was 23.6% of all allocation and 0% of # the live heap, and a leaked watcher goroutine that was ~0% of allocation and # ~80% of the live heap. Unit tests passed throughout both. # # These assert allocation SHAPE (grows with input, not with input squared) and # that every loop-bound raw-JSON mutation in core/providers has been # classified, so a new provider cannot add the same bug silently. test-memory: needs: [check-skip, detect-changes] if: needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && (needs.detect-changes.outputs.core-needs-release == 'true' || needs.detect-changes.outputs.bifrost-http-needs-release == 'true') runs-on: ubuntu-latest permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: *test-core-egress - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # This job uploads JUnit reports written by checked-out test code, and # actions/checkout persists its token in git config by default. Dropping it # removes the only path by which that token could reach an artifact. persist-credentials: false - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.27.3" - name: Run memory regression tests run: | # Transport tests need the core/framework/plugins from this checkout. source ./.github/workflows/scripts/setup-go-workspace.sh make test-memory - name: Upload memory test report if: ${{ !cancelled() }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: memory-regression-report path: test-reports/memory-*.xml retention-days: 30 # Break-glass for the memory gate. # # Unlike a flaky provider call, these assertions are deterministic: a failure # here is a real regression, not noise. This exists so a release is never # permanently wedged by it, but approving means consciously shipping a known # memory regression. Prefer fixing forward. approve-memory-regression: needs: [check-skip, detect-changes, test-memory] if: | always() && needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && (needs.detect-changes.outputs.core-needs-release == 'true' || needs.detect-changes.outputs.bifrost-http-needs-release == 'true') && needs.test-memory.result == 'failure' runs-on: ubuntu-latest environment: name: memory-regression-override url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} outputs: approved: ${{ steps.approve.outputs.approved }} steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: >+ - name: Display failed test info run: | echo "::warning::test-memory failed. These assertions are deterministic - this is a real regression, not a flake." echo "Download the memory-regression-report artifact to see which assertion failed." echo "" echo "An allocation-scaling failure means a function now allocates with the SQUARE of its input." echo "The usual cause is an sjson Set/Delete inside a loop: each call reserialises the whole" echo "document, so N edits cost N copies of it. Batch the edits into a single write instead." echo "" echo "A coverage failure means a loop-bound raw-JSON mutation was added without being classified." echo "Add it to reviewedSites in core/internal/memtest/coverage_test.go." echo "" echo "Approving ships a known memory regression. Prefer fixing forward." - name: Mark as approved id: approve run: echo "approved=true" >> $GITHUB_OUTPUT # Resolves the three most recent published versions of each coding CLI, fresh # on every run - nothing is pinned in the repo. Its output fans out into # test-cli-harness's matrix below. # # This exists as its own job so all three legs share ONE npm snapshot. Were # each leg to resolve independently, a release landing mid-run would make one # leg's latest-1 equal to another's latest, and the run would test the same # version twice while reporting three-version coverage. resolve-cli-versions: needs: [check-skip, detect-changes] # Must track test-cli-harness's gate exactly - this job exists only to build # that matrix, so running it when the harness is skipped resolves versions # nothing consumes. if: | needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && needs.detect-changes.outputs.core-needs-release == 'true' runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read outputs: matrix: ${{ steps.resolve.outputs.matrix }} steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > api.github.com:443 azure.archive.ubuntu.com:80 esm.ubuntu.com:443 github.com:443 iojs.org:443 nodejs.org:443 registry.npmjs.org:443 release-assets.githubusercontent.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: "25" - name: Install jq run: | sudo apt-get update sudo apt-get install -y jq - name: Resolve CLI versions id: resolve run: | chmod +x ./.github/workflows/scripts/resolve-cli-versions.sh ./.github/workflows/scripts/resolve-cli-versions.sh # CLI harness - installs the real Claude Code, Codex and OpenCode CLIs and # drives them as subprocesses against a live Bifrost, asserting on their # non-interactive stream-JSON output (tests/e2e/clis). Scope is deliberately # narrow: one model per CLI and a core scenario set. See test-cli-harness.sh # for why, and to widen it. # # Four suites, not three: OpenCode runs twice, once per wire format. The # `opencode` suite is chat/completions and the `opencode-responses` suite is # /v1/responses against Anthropic-on-Bedrock. Bifrost converts requests # separately per wire format, and the Responses conversion had NO coverage # until that suite existed - codex is the only other Responses client and it # is gated to openai. A reasoning-replay conversion defect shipped on that # uncovered path while this job was green. # # Runs once per CLI version generation (latest, latest-1, latest-2), each an # independent job over its own version triple, so a regression can be pinned # to a specific upstream release rather than just "the CLIs changed". # fail-fast is off: an upstream break in the newest claude must not cancel the # legs that would have shown latest-1 and latest-2 still passing. # # Note that codex floats here too, while tests/e2e/clis/matrix_test.go # documents its --image and model_reasoning_effort assertions specifically # against rust-v0.145.0. A codex leg failing on those is the signal to go # re-check those comments against the newer release. # # The console shows a progress table refreshed every 10s (one row per CLI: # total/done/pass/fail); each suite's verbose output is redirected to # tmp/cli-harness-.log and uploaded with the reports below. test-cli-harness: needs: [check-skip, detect-changes, resolve-cli-versions, build-gateway] # Gated on core alone, matching test-core. What this harness actually covers # is core's request/response conversion per wire format, so a release with no # new core has nothing here to regress. Downstream release jobs already # accept `needs.test-cli-harness.result == 'skipped'`. if: | needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && needs.detect-changes.outputs.core-needs-release == 'true' strategy: fail-fast: false matrix: include: ${{ fromJSON(needs.resolve-cli-versions.outputs.matrix) }} name: test-cli-harness (${{ matrix.label }}) runs-on: ubuntu-latest # The five run_cases invocations in test-cli-harness.sh each pass # TIMEOUT=25m, so the Go suites alone can take 125m on top of the CLI # installs, the UI build and the gateway build. A job timeout cancels the # run, and the report upload is `if: !cancelled()`, so a cancel loses the # harness artifacts too. Raised twice: 90 -> 120 when the opencode suite was # added and the core scenarios went multi-turn, 120 -> 150 for the # opencode-responses suite, 150 -> 180 for opencode-anthropic. timeout-minutes: 180 permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block # The CLIs run as host subprocesses and reach providers *through* # Bifrost at 127.0.0.1:8080, so provider hosts are needed for the # gateway's own upstream calls. statsig.anthropic.com and # o33249.ingest.sentry.io are Claude Code's telemetry endpoints - it # sets CLAUDE_CODE_DISABLE_TELEMETRY=1 but still resolves them on boot. # # models.dev is OpenCode's provider/model catalog # (https://opencode.ai/docs/models/), fetched at startup and cached # under XDG_CACHE_HOME - which the harness points at a fresh temp dir # per cell (opencodeResume.Start), so every cell re-fetches it rather # than hitting a warm cache. OpenCode also resolves its provider SDK, # @ai-sdk/openai-compatible, from registry.npmjs.org at runtime (see # opencodePreLaunch and https://opencode.ai/docs/providers/); that # host is already allowed for the npm installs. allowed-endpoints: > 127.0.0.1:8080 7defe2860d5ee49a1e667e1eeea34b25.r2.cloudflarestorage.com:443 api.anthropic.com:443 api.cerebras.ai:443 api.minimax.io:443 ark.ap-southeast.bytepluses.com:443 api.cohere.ai:443 api.deepseek.com:443 api.elevenlabs.io:443 api.github.com:443 api.groq.com:443 api.mistral.ai:443 api.openai.com:443 api.parasail.io:443 api.tokenfactory.nebius.com:443 azure.archive.ubuntu.com:80 esm.ubuntu.com:443 bedrock-runtime.us-east-1.amazonaws.com:443 bedrock.us-east-1.amazonaws.com:443 bifrost-gateway-resource.openai.azure.com:443 fonts.googleapis.com:443 fonts.gstatic.com:443 generativelanguage.googleapis.com:443 getbifrost.ai:443 github.com:443 iojs.org:443 files.pythonhosted.org:443 openrouter.ai:443 pypi.org:443 models.dev:443 nodejs.org:443 o33249.ingest.sentry.io:443 proxy.golang.org:443 registry.npmjs.org:443 release-assets.githubusercontent.com:443 statsig.anthropic.com:443 storage.googleapis.com:443 sum.golang.org:443 www.getbifrost.ai:443 - 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.27.3" - name: Set up Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "25" - name: Install jq run: | sudo apt-get update sudo apt-get install -y jq # Built once by build-gateway. All three legs share the same binary, so a # regression can never be blamed on one leg having linked a different one. - name: Download prebuilt bifrost-http binary uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: bifrost-http-binary path: tmp # upload-artifact does not preserve the executable bit. - name: Make bifrost-http executable run: chmod +x tmp/bifrost-http - name: Run CLI harness env: # Tells harness_build_gateway to use tmp/bifrost-http as-is. SKIP_GATEWAY_BUILD: "1" OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} # Azure serves both the GPT and the Claude legs of the matrix from one # AIServices resource: Bifrost routes OpenAI-family models to # /openai/deployments//chat/completions and Anthropic-family # models to /anthropic/v1/messages off the same AZURE_ENDPOINT host # (core/providers/azure/azure.go). One endpoint, one key, both wire # formats - so no second secret is needed for the claude/azure cells. AZURE_API_KEY: ${{ secrets.AZURE_API_KEY }} AZURE_ENDPOINT: ${{ secrets.AZURE_ENDPOINT }} AZURE_API_VERSION: ${{ secrets.AZURE_API_VERSION }} # Bedrock, for the opencode-responses suite. The harness gateway seeds # from tests/integrations/python/config.json, whose bedrock_key_config # reads exactly these env vars (the session token matters when the # credentials are temporary STS ones). Without them the provider is # unconfigured and TestCLIs SKIPS every bedrock cell - silently, and # with a green suite, which is the failure mode that let the reported # Responses defect ship in the first place. 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_REGION: ${{ secrets.AWS_REGION }} BIFROST_ENCRYPTION_KEY: ${{ secrets.BIFROST_ENCRYPTION_KEY }} CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }} MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }} COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GOOGLE_LOCATION: ${{ secrets.GOOGLE_LOCATION }} GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} HUGGING_FACE_API_KEY: ${{ secrets.HUGGING_FACE_API_KEY }} MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} NEBIUS_API_KEY: ${{ secrets.NEBIUS_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} PARASAIL_API_KEY: ${{ secrets.PARASAIL_API_KEY }} PERPLEXITY_API_KEY: ${{ secrets.PERPLEXITY_API_KEY }} REPLICATE_API_KEY: ${{ secrets.REPLICATE_API_KEY }} RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }} VERTEX_CREDENTIALS: ${{ secrets.VERTEX_CREDENTIALS }} VERTEX_PROJECT_ID: ${{ secrets.VERTEX_PROJECT_ID }} XAI_API_KEY: ${{ secrets.XAI_API_KEY }} # Resolved fresh by resolve-cli-versions on every run; the constants # in test-cli-harness.sh only apply if that job had to fall back. CLAUDE_CODE_VERSION: ${{ matrix.claude }} CODEX_VERSION: ${{ matrix.codex }} OPENCODE_VERSION: ${{ matrix.opencode }} # Distinguishes this leg's section of the shared job summary. CLI_VERSION_LABEL: ${{ matrix.label }} run: | chmod +x ./.github/workflows/scripts/test-cli-harness.sh ./.github/workflows/scripts/test-cli-harness.sh - name: Upload CLI harness reports if: ${{ !cancelled() }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: # Per-leg name: upload-artifact rejects a duplicate name outright, so # a shared one would fail every leg after the first. name: cli-harness-reports-${{ matrix.label }} path: | tests/e2e/clis/reports/ tmp/bifrost-cli-harness.log tmp/cli-harness-claude.log tmp/cli-harness-codex.log tmp/cli-harness-opencode.log tmp/cli-harness-opencode-responses.log tmp/cli-harness-opencode-anthropic.log # Per-attempt snapshots, written when a suite needed retries. The # reports above are overwritten in place by each rerun, so this is # the only record of what the first attempt failed on. tmp/cli-harness-attempts/ retention-days: 30 # Publishes the same report to R2 so the Mintlify release notes can link # it. The artifact above needs a GitHub login and expires; R2 does not. # Never fails the job - see the script's header. - name: Publish CLI harness report if: ${{ !cancelled() }} env: 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: | chmod +x ./.github/workflows/scripts/configure-r2.sh ./.github/workflows/scripts/upload-test-reports-to-r2.sh ./.github/workflows/scripts/configure-r2.sh || { echo "::warning::R2 not configured; skipping report publish"; exit 0; } ./.github/workflows/scripts/upload-test-reports-to-r2.sh \ "${{ needs.detect-changes.outputs.transport-version }}" \ "cli-harness-${{ matrix.label }}" \ tests/e2e/clis/reports # vLLM provider tests - allocates two vLLM (one stable and one nightly) instances # on runpod (runpodctl): test-vllm: needs: [check-skip, detect-changes] if: needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && needs.detect-changes.outputs.core-needs-release == 'true' runs-on: ubuntu-latest timeout-minutes: 60 permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > api.github.com:443 github.com:443 release-assets.githubusercontent.com:443 api.runpod.io:443 rest.runpod.io:443 *.proxy.runpod.net:443 proxy.golang.org:443 storage.googleapis.com:443 sum.golang.org:443 - 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.27.3" - name: Create vLLM pods on RunPod env: RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }} RUNPOD_NETWORK_VOLUME_ID: ${{ secrets.RUNPOD_NETWORK_VOLUME_ID }} RUNPOD_REASONING_NETWORK_VOLUME_ID: ${{ secrets.RUNPOD_REASONING_NETWORK_VOLUME_ID }} run: | chmod +x ./.github/workflows/scripts/setup-vllm-runpod.sh ./.github/workflows/scripts/setup-vllm-runpod.sh - name: Run vLLM provider tests run: VLLM_ENABLED=1 make test-core PROVIDER=vllm - name: Delete vLLM pods on RunPod if: always() env: RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }} run: | chmod +x ./.github/workflows/scripts/teardown-vllm-runpod.sh ./.github/workflows/scripts/teardown-vllm-runpod.sh test-framework: needs: [check-skip, detect-changes] if: needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && needs.detect-changes.outputs.framework-needs-release == 'true' runs-on: ubuntu-latest permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > _grpc_config.cluster.qdrant.io:443 _grpc_config.localhost:443 api.cerebras.ai:443 api.minimax.io:443 ark.ap-southeast.bytepluses.com:443 api.cohere.ai:443 api.deepseek.com:443 api.elevenlabs.io:443 api.github.com:443 api.groq.com:443 api.mistral.ai:443 api.parasail.io:443 api.tokenfactory.nebius.com:443 auth.docker.io:443 cluster.qdrant.io:443 cluster.weaviate.network:443 codecov.io:443 dl-cdn.alpinelinux.org:443 generativelanguage.googleapis.com:443 ghcr.io:443 github.com:443 openrouter.ai:443 pkg-containers.githubusercontent.com:443 production.cloudflare.docker.com:443 production.cloudfront.docker.com:443 proxy.golang.org:443 registry-1.docker.io:443 release-assets.githubusercontent.com:443 storage.googleapis.com:443 sum.golang.org:443 telemetry.qdrant.io:443 uploader.codecov.io:443 - 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.27.3" - name: Set up Docker Compose run: | docker --version if ! docker compose version >/dev/null 2>&1; then echo "Installing Docker Compose..." ./.github/workflows/scripts/install-docker-compose.sh /usr/local/bin/docker-compose docker-compose --version else echo "Docker Compose plugin is available" docker compose version fi - name: Run framework tests env: 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 }} AZURE_API_VERSION: ${{ secrets.AZURE_API_VERSION }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} 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 }} ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }} PERPLEXITY_API_KEY: ${{ secrets.PERPLEXITY_API_KEY }} SGL_API_KEY: ${{ secrets.SGL_API_KEY }} CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }} MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }} SARVAM_API_KEY: ${{ secrets.SARVAM_API_KEY }} TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }} DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} DATABRICKS_WORKSPACE_URL: ${{ secrets.DATABRICKS_WORKSPACE_URL }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 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 }} VERTEX_GCS_BUCKET: ${{ secrets.VERTEX_GCS_BUCKET }} VERTEX_GCS_PREFIX: ${{ secrets.VERTEX_GCS_PREFIX }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} HUGGING_FACE_API_KEY: ${{ secrets.HUGGING_FACE_API_KEY }} BIFROST_ENCRYPTION_KEY: ${{ secrets.BIFROST_ENCRYPTION_KEY }} AWS_REGION: ${{ secrets.AWS_REGION }} DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} GOOGLE_LOCATION: ${{ secrets.GOOGLE_LOCATION }} NEBIUS_API_KEY: ${{ secrets.NEBIUS_API_KEY }} REPLICATE_API_KEY: ${{ secrets.REPLICATE_API_KEY }} RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }} XAI_API_KEY: ${{ secrets.XAI_API_KEY }} run: ./.github/workflows/scripts/test-framework.sh test-plugins: needs: [check-skip, detect-changes] if: needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && needs.detect-changes.outputs.plugins-need-release == 'true' runs-on: ubuntu-latest permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > _grpc_config.localhost:443 _http._tcp.azure.archive.ubuntu.com:443 _https._tcp.esm.ubuntu.com:443 _https._tcp.dl.google.com:443 _https._tcp.motd.ubuntu.com:443 _https._tcp.packages.microsoft.com:443 api.cerebras.ai:443 api.minimax.io:443 ark.ap-southeast.bytepluses.com:443 api.cohere.ai:443 api.deepseek.com:443 api.elevenlabs.io:443 api.groq.com:443 api.mistral.ai:443 api.parasail.io:443 api.tokenfactory.nebius.com:443 generativelanguage.googleapis.com:443 openrouter.ai:443 production.cloudfront.docker.com:443 dl.google.com:443 api.github.com:443 api.openai.com:443 auth.docker.io:443 azure.archive.ubuntu.com:80 codecov.io:443 dl-cdn.alpinelinux.org:443 esm.ubuntu.com:443 getbifrost.ai:443 ghcr.io:443 github.com:443 packages.microsoft.com:443 pkg-containers.githubusercontent.com:443 production.cloudflare.docker.com:443 proxy.golang.org:443 registry-1.docker.io:443 release-assets.githubusercontent.com:443 storage.googleapis.com:443 sum.golang.org:443 telemetry.qdrant.io:443 uploader.codecov.io:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 fetch-tags: true - name: Install jq run: | sudo apt-get update sudo apt-get install -y jq - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.27.3" - name: Set up Docker Compose run: | docker --version if ! docker compose version >/dev/null 2>&1; then echo "Installing Docker Compose..." ./.github/workflows/scripts/install-docker-compose.sh /usr/local/bin/docker-compose docker-compose --version else echo "Docker Compose plugin is available" docker compose version fi - name: Run plugin tests env: 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 }} AZURE_API_VERSION: ${{ secrets.AZURE_API_VERSION }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} 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 }} ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }} PERPLEXITY_API_KEY: ${{ secrets.PERPLEXITY_API_KEY }} SGL_API_KEY: ${{ secrets.SGL_API_KEY }} CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }} MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }} SARVAM_API_KEY: ${{ secrets.SARVAM_API_KEY }} TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }} DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} DATABRICKS_WORKSPACE_URL: ${{ secrets.DATABRICKS_WORKSPACE_URL }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 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 }} VERTEX_GCS_BUCKET: ${{ secrets.VERTEX_GCS_BUCKET }} VERTEX_GCS_PREFIX: ${{ secrets.VERTEX_GCS_PREFIX }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} HUGGING_FACE_API_KEY: ${{ secrets.HUGGING_FACE_API_KEY }} BIFROST_ENCRYPTION_KEY: ${{ secrets.BIFROST_ENCRYPTION_KEY }} AWS_REGION: ${{ secrets.AWS_REGION }} DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} GOOGLE_LOCATION: ${{ secrets.GOOGLE_LOCATION }} NEBIUS_API_KEY: ${{ secrets.NEBIUS_API_KEY }} REPLICATE_API_KEY: ${{ secrets.REPLICATE_API_KEY }} RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }} XAI_API_KEY: ${{ secrets.XAI_API_KEY }} run: ./.github/workflows/scripts/test-all-plugins.sh test-bifrost-http: needs: [check-skip, detect-changes] if: needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && needs.detect-changes.outputs.bifrost-http-needs-release == 'true' runs-on: ubuntu-latest permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > api.cerebras.ai:443 api.minimax.io:443 ark.ap-southeast.bytepluses.com:443 api.cohere.ai:443 api.deepseek.com:443 api.elevenlabs.io:443 api.groq.com:443 api.mistral.ai:443 api.parasail.io:443 api.tokenfactory.nebius.com:443 generativelanguage.googleapis.com:443 iojs.org:443 api.github.com:443 codecov.io:443 example.com:443 file.example.com:443 fonts.googleapis.com:443 fonts.gstatic.com:443 getbifrost.ai:443 github.com:443 nodejs.org:443 openrouter.ai:443 proxy.golang.org:443 registry.npmjs.org:443 release-assets.githubusercontent.com:443 storage.googleapis.com:443 sum.golang.org:443 uploader.codecov.io:443 www.getbifrost.ai:443 - 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.27.3" - name: Run bifrost-http unit tests env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} BIFROST_ENCRYPTION_KEY: ${{ secrets.BIFROST_ENCRYPTION_KEY }} run: ./.github/workflows/scripts/test-bifrost-http.sh # GPT Live e2e suite (tests/live): the prebuilt gateway on the live profile, which points the # OpenAI provider at the suite's fake upstream, so no provider keys are needed. The long # 17-minute sessions run nightly from live-tests.yml instead. test-live: needs: [check-skip, detect-changes, build-gateway] if: needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && needs.detect-changes.outputs.bifrost-http-needs-release == 'true' runs-on: ubuntu-latest timeout-minutes: 20 permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block # stun.l.google.com is the STUN server the WebRTC live tests use for # ICE candidate gathering. allowed-endpoints: > 127.0.0.1:8080 127.0.0.1:9411 api.github.com:443 files.pythonhosted.org:443 getbifrost.ai:443 github.com:443 objects.githubusercontent.com:443 proxy.golang.org:443 pypi.org:443 release-assets.githubusercontent.com:443 releases.astral.sh:443 storage.googleapis.com:443 stun.l.google.com:19302 sum.golang.org:443 www.getbifrost.ai:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.27.3" # The suite's OpenAI Python SDK leg runs through uv; without it those cases skip. - name: Install uv uses: step-security/setup-uv@ccf0a26ce9117d9e99292b0ce953ea5d9ffe778e # v7.3.0 with: version: "0.11.0" python-version: "3.12" - name: Download prebuilt bifrost-http binary uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: bifrost-http-binary path: tmp - name: Make bifrost-http executable run: chmod +x tmp/bifrost-http - name: Run GPT Live e2e tests env: LIVE_PREBUILT: "1" run: ./.github/workflows/scripts/test-live.sh fake - name: Upload live test report if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: live-test-report path: | test-reports/live.xml tmp/live-gateway.log if-no-files-found: ignore # Migration tests - validates database migrations from previous versions test-migrations: needs: [check-skip, detect-changes, build-gateway] if: needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && needs.detect-changes.outputs.bifrost-http-needs-release == 'true' runs-on: ubuntu-latest permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block # Migration test binary runs on the host (not in a container) and # connects to Postgres via the published port mapping (5432:5432) at # 127.0.0.1 (see run-migration-tests.sh:46, POSTGRES_HOST defaults to # localhost). harden-runner filters on the post-NAT destination IP, so # the published port DNATs to Postgres's pinned bridge IP 172.38.0.11 - # that IP:port must be allowed. Keep in sync with the pinned IP in # .github/workflows/configs/docker-compose.yml. (The 172.38.0.12 # weaviate entries are vestigial here - this job only starts postgres.) allowed-endpoints: > 127.0.0.1:5432 172.38.0.11:5432 172.38.0.12:8080 172.38.0.12:8301 api.anthropic.com:443 api.cerebras.ai:443 api.minimax.io:443 ark.ap-southeast.bytepluses.com:443 api.cohere.ai:443 api.deepseek.com:443 api.elevenlabs.io:443 api.github.com:443 api.groq.com:443 api.mistral.ai:443 api.openai.com:443 api.parasail.io:443 api.tokenfactory.nebius.com:443 auth.docker.io:443 azure.archive.ubuntu.com:80 downloads.getmaxim.ai:443 esm.ubuntu.com:443 generativelanguage.googleapis.com:443 getbifrost.ai:443 github.com:443 nodejs.org:443 openrouter.ai:443 packages.microsoft.com:443 production.cloudflare.docker.com:443 production.cloudfront.docker.com:443 proxy.golang.org:443 registry-1.docker.io:443 registry.npmjs.org:443 release-assets.githubusercontent.com:443 storage.googleapis.com:443 sum.golang.org:443 - 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.27.3" - 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..." ./.github/workflows/scripts/install-docker-compose.sh /usr/local/bin/docker-compose docker-compose --version else echo "Docker Compose plugin is available" docker compose version fi # Only the "current version" binary comes from here; the previous-version # binaries this job migrates from are still downloaded per release. - name: Download prebuilt bifrost-http binary uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: bifrost-http-binary path: tmp - name: Make bifrost-http executable run: chmod +x tmp/bifrost-http # The SQLite migration tests seed and read the stores through the sqlite3 CLI - name: Install sqlite3 run: | sudo apt-get update sudo apt-get install -y sqlite3 sqlite3 --version - name: Run migration tests (PostgreSQL) env: SKIP_GATEWAY_BUILD: "1" run: | chmod +x ./.github/workflows/scripts/run-migration-tests.sh ./.github/workflows/scripts/run-migration-tests.sh postgres - name: Run migration tests (SQLite) env: SKIP_GATEWAY_BUILD: "1" run: ./.github/workflows/scripts/run-migration-tests.sh sqlite # Agent Gateway lifecycle e2e - runs the prebuilt gateway on SQLite with # private push callbacks enabled and exercises tests/agentgateway. test-agentgateway: needs: [check-skip, detect-changes, build-gateway] if: needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && needs.detect-changes.outputs.bifrost-http-needs-release == 'true' runs-on: ubuntu-latest permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block # getbifrost.ai / www.getbifrost.ai serve the pricing datasheet the # gateway must sync at boot; without them startup is fatal. allowed-endpoints: > 127.0.0.1 getbifrost.ai:443 github.com:443 proxy.golang.org:443 release-assets.githubusercontent.com:443 storage.googleapis.com:443 sum.golang.org:443 www.getbifrost.ai:443 - 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.27.3" - name: Download prebuilt bifrost-http binary uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: bifrost-http-binary path: tmp - name: Make bifrost-http executable run: chmod +x tmp/bifrost-http - name: Run Agent Gateway e2e env: SKIP_GATEWAY_BUILD: "1" run: | chmod +x ./.github/workflows/scripts/test-agentgateway-e2e.sh ./.github/workflows/scripts/test-agentgateway-e2e.sh # API integrations tests - compiles bifrost-http, runs against Postgres with # tests/config.json, executes the api-management newman collection. test-api-integrations: needs: [check-skip, detect-changes, build-gateway] # Provider-secret redaction spans core schemas, framework storage, and HTTP responses. if: needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && (needs.detect-changes.outputs.core-needs-release == 'true' || needs.detect-changes.outputs.framework-needs-release == 'true' || needs.detect-changes.outputs.bifrost-http-needs-release == 'true') runs-on: ubuntu-latest permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block # API integrations test compiles bifrost-http and runs it on the host # (not in a container), connecting to Postgres via the published port # mapping (5432:5432) at 127.0.0.1. See test-api-integrations.sh which # defaults POSTGRES_HOST to localhost. harden-runner filters on the # post-NAT destination IP, so the published port DNATs to Postgres's # pinned bridge IP 172.38.0.11 - that IP:port must be allowed. This # script does `docker compose up -d` (ALL services) and waits for them # to be healthy, so weaviate's gossip to its own advertise address # 172.38.0.12:8301 (and 8080) must also be allowed or weaviate never # becomes healthy. The semantic cache suite also reaches Redis Stack # through its published 6379, which DNATs to 172.38.0.13. Keep the # pinned IPs in sync with .github/workflows/configs/docker-compose.yml. allowed-endpoints: > 127.0.0.1:5432 127.0.0.1:6379 172.38.0.11:5432 172.38.0.12:8080 172.38.0.12:8301 172.38.0.13:6379 api.tokenfactory.nebius.com:443 iojs.org:443 aiplatform.googleapis.com:443 api.anthropic.com:443 api.cerebras.ai:443 api.minimax.io:443 ark.ap-southeast.bytepluses.com:443 api.cohere.ai:443 api.deepseek.com:443 api.elevenlabs.io:443 api.github.com:443 api.githubcopilot.com:443 api.business.githubcopilot.com:443 api.enterprise.githubcopilot.com:443 api.individual.githubcopilot.com:443 api.groq.com:443 api.mistral.ai:443 api.openai.com:443 api.parasail.io:443 api.perplexity.ai:443 api.replicate.com:443 api.x.ai:443 auth.docker.io:443 bedrock-mantle.us-east-1.api.aws:443 bedrock-runtime.us-east-1.amazonaws.com:443 bedrock.us-east-1.amazonaws.com:443 bifrost-gateway-resource.openai.azure.com:443 cr.weaviate.io:443 dl-cdn.alpinelinux.org:443 fonts.googleapis.com:443 fonts.gstatic.com:443 generativelanguage.googleapis.com:443 getbifrost.ai:443 ghcr.io:443 github.com:443 huggingface.co:443 maxim-o1.openai.azure.com:443 nodejs.org:443 oauth2.googleapis.com:443 openrouter.ai:443 pkg-containers.githubusercontent.com:443 production.cloudflare.docker.com:443 production.cloudfront.docker.com:443 proxy.golang.org:443 registry-1.docker.io:443 registry.npmjs.org:443 release-assets.githubusercontent.com:443 storage.googleapis.com:443 sum.golang.org:443 telemetry.qdrant.io:443 www.getbifrost.ai:443 - 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.27.3" - name: Set up Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "25" # npm ci against .github/workflows/tools/package-lock.json, not `npm install -g`: # the lockfile carries an integrity hash for newman and every transitive dep, which # a bare version pin does not. Bin dir goes on PATH because the test scripts probe # for newman with `command -v`. - name: Install Newman (hash-pinned via lockfile) run: | npm ci --no-audit --no-fund echo "$PWD/node_modules/.bin" >> "$GITHUB_PATH" working-directory: .github/workflows/tools # The content-logging matrix reads raw log rows from the Postgres logs store through `pg`, # pinned in tests/e2e/api/package-lock.json. --ignore-scripts skips better-sqlite3's native # build: this job's logs store is Postgres, so the SQLite reader is never loaded. - name: Install e2e API test deps (lockfile) run: npm ci --ignore-scripts --no-audit --no-fund working-directory: tests/e2e/api - name: Set up Docker Compose run: | docker --version if ! docker compose version >/dev/null 2>&1; then echo "Installing Docker Compose..." ./.github/workflows/scripts/install-docker-compose.sh /usr/local/bin/docker-compose docker-compose --version else echo "Docker Compose plugin is available" docker compose version fi - name: Download prebuilt bifrost-http binary uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: bifrost-http-binary path: tmp - name: Make bifrost-http executable run: chmod +x tmp/bifrost-http - name: Run API integrations test env: CI: "1" SKIP_GATEWAY_BUILD: "1" BIFROST_SETUP_TOKEN: bifrost-e2e-setup-token # Synthetic, non-secret canary for every provider's API redaction tests. BIFROST_SECRET_REDACTION_CANARY: synthetic-secretvar-harness-canary-0123456789 # Key server URLs (ollama/sgl/vllm) must resolve to http(s), so they use a URL canary. BIFROST_SECRET_REDACTION_URL_CANARY: http://127.0.0.1/synthetic-secretvar-harness-canary-0123456789 OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} VERTEX_PROJECT_ID: ${{ secrets.VERTEX_PROJECT_ID }} VERTEX_CREDENTIALS: ${{ secrets.VERTEX_CREDENTIALS }} VERTEX_GCS_BUCKET: ${{ secrets.VERTEX_GCS_BUCKET }} VERTEX_GCS_PREFIX: ${{ secrets.VERTEX_GCS_PREFIX }} GOOGLE_LOCATION: ${{ secrets.GOOGLE_LOCATION }} MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} PERPLEXITY_API_KEY: ${{ secrets.PERPLEXITY_API_KEY }} CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }} MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }} DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} SARVAM_API_KEY: ${{ secrets.SARVAM_API_KEY }} TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }} DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} DATABRICKS_WORKSPACE_URL: ${{ secrets.DATABRICKS_WORKSPACE_URL }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} GITHUB_COPILOT_API_KEY: ${{ secrets.GITHUB_COPILOT_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} PARASAIL_API_KEY: ${{ secrets.PARASAIL_API_KEY }} ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }} FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }} HUGGING_FACE_API_KEY: ${{ secrets.HUGGING_FACE_API_KEY }} XAI_API_KEY: ${{ secrets.XAI_API_KEY }} REPLICATE_API_KEY: ${{ secrets.REPLICATE_API_KEY }} AZURE_API_KEY: ${{ secrets.AZURE_API_KEY }} AZURE_ENDPOINT: ${{ secrets.AZURE_ENDPOINT }} AZURE_API_VERSION: ${{ secrets.AZURE_API_VERSION }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_REGION: ${{ secrets.AWS_REGION }} AWS_ARN: ${{ secrets.AWS_ARN }} NEBIUS_API_KEY: ${{ secrets.NEBIUS_API_KEY }} RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }} run: | chmod +x ./.github/workflows/scripts/test-api-integrations.sh ./.github/workflows/scripts/test-api-integrations.sh - name: Upload Newman reports if: ${{ !cancelled() }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: newman-reports-api-integrations path: tests/e2e/api/newman-reports/ retention-days: 30 # Cost accuracy test - validates logged LLM costs against provider usage test-cost-accuracy: needs: [check-skip, detect-changes, build-gateway] if: | needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && ( needs.detect-changes.outputs.core-needs-release == 'true' || needs.detect-changes.outputs.framework-needs-release == 'true' || needs.detect-changes.outputs.plugins-need-release == 'true' || needs.detect-changes.outputs.bifrost-http-needs-release == 'true' || needs.detect-changes.outputs.docker-needs-release == 'true' ) runs-on: ubuntu-latest permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > 127.0.0.1:5432 127.0.0.1:8000 127.0.0.1:8080 172.38.0.11:5432 api.cerebras.ai:443 api.minimax.io:443 ark.ap-southeast.bytepluses.com:443 api.cohere.ai:443 api.deepseek.com:443 api.elevenlabs.io:443 api.github.com:443 api.groq.com:443 api.mistral.ai:443 api.parasail.io:443 api.tokenfactory.nebius.com:443 auth.docker.io:443 codeload.github.com:443 generativelanguage.googleapis.com:443 getbifrost.ai:443 github.com:443 objects.githubusercontent.com:443 openrouter.ai:443 production.cloudflare.docker.com:443 production.cloudfront.docker.com:443 proxy.golang.org:443 registry-1.docker.io:443 release-assets.githubusercontent.com:443 storage.googleapis.com:443 sum.golang.org:443 www.getbifrost.ai:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 fetch-tags: true persist-credentials: false - name: Checkout bifrost-benchmarking uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: maximhq/bifrost-benchmarking path: bifrost-benchmarking persist-credentials: false - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.27.3" # This job still builds the mocker/hitter binaries from # bifrost-benchmarking itself - only bifrost-http comes prebuilt. - name: Download prebuilt bifrost-http binary uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: bifrost-http-binary path: tmp - name: Make bifrost-http executable run: chmod +x tmp/bifrost-http - name: Run cost accuracy test env: SKIP_GATEWAY_BUILD: "1" BENCHMARK_DIR: ${{ github.workspace }}/bifrost-benchmarking run: | chmod +x ./.github/workflows/scripts/cost-accuracy-test.sh ./.github/workflows/scripts/cost-accuracy-test.sh - name: Upload cost accuracy artifacts if: ${{ !cancelled() }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: cost-accuracy-results path: tmp/cost-accuracy/ retention-days: 30 # Non-blocking load performance test - runs streaming and non-streaming load flows test-load-performance: needs: [check-skip, detect-changes, build-gateway] if: | needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && ( needs.detect-changes.outputs.core-needs-release == 'true' || needs.detect-changes.outputs.framework-needs-release == 'true' || needs.detect-changes.outputs.plugins-need-release == 'true' || needs.detect-changes.outputs.bifrost-http-needs-release == 'true' || needs.detect-changes.outputs.docker-needs-release == 'true' ) runs-on: ubuntu-latest continue-on-error: true timeout-minutes: 30 permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > 127.0.0.1:8000 127.0.0.1:8080 api.cerebras.ai:443 api.minimax.io:443 ark.ap-southeast.bytepluses.com:443 api.cohere.ai:443 api.deepseek.com:443 api.elevenlabs.io:443 api.github.com:443 api.groq.com:443 api.mistral.ai:443 api.parasail.io:443 api.tokenfactory.nebius.com:443 bedrock.us-east-1.amazonaws.com:443 codeload.github.com:443 generativelanguage.googleapis.com:443 getbifrost.ai:443 github.com:443 huggingface.co:443 objects.githubusercontent.com:443 openrouter.ai:443 proxy.golang.org:443 release-assets.githubusercontent.com:443 storage.googleapis.com:443 sum.golang.org:443 www.getbifrost.ai:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 fetch-tags: true persist-credentials: false - name: Checkout bifrost-benchmarking uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: maximhq/bifrost-benchmarking path: bifrost-benchmarking persist-credentials: false - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.27.3" # load-test.sh's build_bifrost_http already no-ops when tmp/bifrost-http # is present, so this download is all that is needed here. - name: Download prebuilt bifrost-http binary uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: bifrost-http-binary path: tmp - name: Make bifrost-http executable run: chmod +x tmp/bifrost-http - name: Run load performance test env: BENCHMARK_DIR: ${{ github.workspace }}/bifrost-benchmarking BIFROST_LOG_LEVEL: "warn" RATE: "1000" STREAMING_RATE: "1000" MAX_WORKERS: "12000" OVERHEAD_DURATION: "10" STRESS_DURATION: "10" OVERHEAD_MOCKER_LATENCY_MS: "500" STRESS_MOCKER_LATENCY_MS: "500" run: | chmod +x ./.github/workflows/scripts/load-test.sh ./.github/workflows/scripts/load-test.sh - name: Upload load test artifacts if: ${{ !cancelled() }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: load-performance-results path: | .github/workflows/scripts/load-test-results.md .github/workflows/scripts/load-test-results.json retention-days: 30 # Docker image test - amd64 test-docker-image-amd64: needs: [check-skip, detect-changes] if: needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && needs.detect-changes.outputs.docker-needs-release == 'true' runs-on: ubuntu-latest permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block # 172.28.0.16:5432 is the pinned postgres IP from # tests/docker-compose.yml (postgres.networks.bifrost_network.ipv4_address). # It's also referenced as the literal "host" value in the config.json # generated by .github/workflows/scripts/test-docker-image.sh - the # hostname "postgres" can't be used because harden-runner interferes # with Docker's embedded DNS at 127.0.0.11:53. Keep all three files # in sync; drift here will block egress to Postgres during the docker # image test. allowed-endpoints: > 127.0.0.11:53 172.28.0.16:5432 api.anthropic.com:443 api.cerebras.ai:443 api.minimax.io:443 ark.ap-southeast.bytepluses.com:443 api.cohere.ai:443 api.deepseek.com:443 api.elevenlabs.io:443 api.github.com:443 api.githubcopilot.com:443 api.business.githubcopilot.com:443 api.enterprise.githubcopilot.com:443 api.individual.githubcopilot.com:443 api.groq.com:443 api.mistral.ai:443 api.openai.com:443 api.parasail.io:443 api.perplexity.ai:443 api.replicate.com:443 api.tokenfactory.nebius.com:443 api.x.ai:443 auth.docker.io:443 bedrock-mantle.us-east-1.api.aws:443 bedrock.us-east-1.amazonaws.com:443 bifrost-gateway-resource.openai.azure.com:443 dl-cdn.alpinelinux.org:443 fonts.googleapis.com:443 fonts.gstatic.com:443 generativelanguage.googleapis.com:443 getbifrost.ai:443 ghcr.io:443 github.com:443 huggingface.co:443 maxim-o1.openai.azure.com:443 nodejs.org:443 oauth2.googleapis.com:443 openrouter.ai:443 pkg-containers.githubusercontent.com:443 production.cloudflare.docker.com:443 production.cloudfront.docker.com:443 proxy.golang.org:443 registry-1.docker.io:443 registry.npmjs.org:443 release-assets.githubusercontent.com:443 storage.googleapis.com:443 sum.golang.org:443 telemetry.qdrant.io:443 www.getbifrost.ai:443 - 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.27.3" - name: Set up Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "25" # npm ci against .github/workflows/tools/package-lock.json, not `npm install -g`: # the lockfile carries an integrity hash for newman and every transitive dep, which # a bare version pin does not. Bin dir goes on PATH because the test scripts probe # for newman with `command -v`. - name: Install Newman (hash-pinned via lockfile) run: | npm ci --no-audit --no-fund echo "$PWD/node_modules/.bin" >> "$GITHUB_PATH" working-directory: .github/workflows/tools - name: Setup Docker Buildx uses: step-security/setup-buildx-action@f931205d68723ad9589fd2a7e2ece238bf9de341 # v4.0.0 - name: Test Docker image (amd64) env: CI: "1" OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} VERTEX_PROJECT_ID: ${{ secrets.VERTEX_PROJECT_ID }} VERTEX_CREDENTIALS: ${{ secrets.VERTEX_CREDENTIALS }} VERTEX_GCS_BUCKET: ${{ secrets.VERTEX_GCS_BUCKET }} VERTEX_GCS_PREFIX: ${{ secrets.VERTEX_GCS_PREFIX }} GOOGLE_LOCATION: ${{ secrets.GOOGLE_LOCATION }} MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} PERPLEXITY_API_KEY: ${{ secrets.PERPLEXITY_API_KEY }} CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }} MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }} SARVAM_API_KEY: ${{ secrets.SARVAM_API_KEY }} TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }} DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} DATABRICKS_WORKSPACE_URL: ${{ secrets.DATABRICKS_WORKSPACE_URL }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} GITHUB_COPILOT_API_KEY: ${{ secrets.GITHUB_COPILOT_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} PARASAIL_API_KEY: ${{ secrets.PARASAIL_API_KEY }} ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }} FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }} HUGGING_FACE_API_KEY: ${{ secrets.HUGGING_FACE_API_KEY }} XAI_API_KEY: ${{ secrets.XAI_API_KEY }} REPLICATE_API_KEY: ${{ secrets.REPLICATE_API_KEY }} AZURE_API_KEY: ${{ secrets.AZURE_API_KEY }} AZURE_ENDPOINT: ${{ secrets.AZURE_ENDPOINT }} AZURE_API_VERSION: ${{ secrets.AZURE_API_VERSION }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_REGION: ${{ secrets.AWS_REGION }} AWS_ARN: ${{ secrets.AWS_ARN }} DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} NEBIUS_API_KEY: ${{ secrets.NEBIUS_API_KEY }} RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }} run: | chmod +x ./.github/workflows/scripts/test-docker-image.sh ./.github/workflows/scripts/test-docker-image.sh linux/amd64 - name: Upload Newman reports if: ${{ !cancelled() }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: newman-reports-amd64 path: tests/e2e/api/newman-reports/ retention-days: 30 # Docker image test - arm64 test-docker-image-arm64: needs: [check-skip, detect-changes] if: needs.check-skip.outputs.should-skip != 'true' && needs.check-skip.outputs.skip-tests != 'true' && needs.detect-changes.outputs.docker-needs-release == 'true' runs-on: ubuntu-24.04-arm permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block # 172.28.0.16:5432 is the pinned postgres IP from # tests/docker-compose.yml (postgres.networks.bifrost_network.ipv4_address). # It's also referenced as the literal "host" value in the config.json # generated by .github/workflows/scripts/test-docker-image.sh - the # hostname "postgres" can't be used because harden-runner interferes # with Docker's embedded DNS at 127.0.0.11:53. Keep all three files # in sync; drift here will block egress to Postgres during the docker # image test. allowed-endpoints: > 127.0.0.11:53 172.28.0.16:5432 api.anthropic.com:443 api.cerebras.ai:443 api.minimax.io:443 ark.ap-southeast.bytepluses.com:443 api.cohere.ai:443 api.deepseek.com:443 api.elevenlabs.io:443 api.github.com:443 api.githubcopilot.com:443 api.business.githubcopilot.com:443 api.enterprise.githubcopilot.com:443 api.individual.githubcopilot.com:443 api.groq.com:443 api.mistral.ai:443 api.openai.com:443 api.parasail.io:443 api.perplexity.ai:443 api.replicate.com:443 api.tokenfactory.nebius.com:443 api.x.ai:443 auth.docker.io:443 bedrock-mantle.us-east-1.api.aws:443 bedrock.us-east-1.amazonaws.com:443 bifrost-gateway-resource.openai.azure.com:443 dl-cdn.alpinelinux.org:443 fonts.googleapis.com:443 fonts.gstatic.com:443 generativelanguage.googleapis.com:443 getbifrost.ai:443 ghcr.io:443 github.com:443 huggingface.co:443 maxim-o1.openai.azure.com:443 nodejs.org:443 oauth2.googleapis.com:443 openrouter.ai:443 pkg-containers.githubusercontent.com:443 production.cloudflare.docker.com:443 production.cloudfront.docker.com:443 proxy.golang.org:443 registry-1.docker.io:443 registry.npmjs.org:443 release-assets.githubusercontent.com:443 storage.googleapis.com:443 sum.golang.org:443 telemetry.qdrant.io:443 www.getbifrost.ai:443 - 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.27.3" - name: Set up Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "25" # npm ci against .github/workflows/tools/package-lock.json, not `npm install -g`: # the lockfile carries an integrity hash for newman and every transitive dep, which # a bare version pin does not. Bin dir goes on PATH because the test scripts probe # for newman with `command -v`. - name: Install Newman (hash-pinned via lockfile) run: | npm ci --no-audit --no-fund echo "$PWD/node_modules/.bin" >> "$GITHUB_PATH" working-directory: .github/workflows/tools - name: Setup Docker Buildx uses: step-security/setup-buildx-action@f931205d68723ad9589fd2a7e2ece238bf9de341 # v4.0.0 - name: Test Docker image (arm64) env: CI: "1" OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} VERTEX_PROJECT_ID: ${{ secrets.VERTEX_PROJECT_ID }} VERTEX_CREDENTIALS: ${{ secrets.VERTEX_CREDENTIALS }} VERTEX_GCS_BUCKET: ${{ secrets.VERTEX_GCS_BUCKET }} VERTEX_GCS_PREFIX: ${{ secrets.VERTEX_GCS_PREFIX }} GOOGLE_LOCATION: ${{ secrets.GOOGLE_LOCATION }} MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} PERPLEXITY_API_KEY: ${{ secrets.PERPLEXITY_API_KEY }} CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }} MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }} SARVAM_API_KEY: ${{ secrets.SARVAM_API_KEY }} TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }} DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} DATABRICKS_WORKSPACE_URL: ${{ secrets.DATABRICKS_WORKSPACE_URL }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} GITHUB_COPILOT_API_KEY: ${{ secrets.GITHUB_COPILOT_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} PARASAIL_API_KEY: ${{ secrets.PARASAIL_API_KEY }} ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }} FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }} HUGGING_FACE_API_KEY: ${{ secrets.HUGGING_FACE_API_KEY }} XAI_API_KEY: ${{ secrets.XAI_API_KEY }} REPLICATE_API_KEY: ${{ secrets.REPLICATE_API_KEY }} AZURE_API_KEY: ${{ secrets.AZURE_API_KEY }} AZURE_ENDPOINT: ${{ secrets.AZURE_ENDPOINT }} AZURE_API_VERSION: ${{ secrets.AZURE_API_VERSION }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_REGION: ${{ secrets.AWS_REGION }} AWS_ARN: ${{ secrets.AWS_ARN }} DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} NEBIUS_API_KEY: ${{ secrets.NEBIUS_API_KEY }} RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }} run: | chmod +x ./.github/workflows/scripts/test-docker-image.sh ./.github/workflows/scripts/test-docker-image.sh linux/arm64 - name: Upload Newman reports if: ${{ !cancelled() }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: newman-reports-arm64 path: tests/e2e/api/newman-reports/ retention-days: 30 core-release: needs: [ check-skip, detect-changes, test-core, test-core-integrations, test-core-unit, approve-flaky-test-core, test-proxy-e2e, test-memory, approve-memory-regression, resolve-cli-versions, test-cli-harness, test-vllm, test-framework, test-plugins, test-bifrost-http, test-live, test-migrations, test-agentgateway, test-api-integrations, test-cost-accuracy, test-docker-image-amd64, test-docker-image-arm64, ] if: "always() && needs.check-skip.outputs.should-skip != 'true' && needs.detect-changes.result == 'success' && needs.detect-changes.outputs.core-needs-release == 'true' && (needs.test-core.result == 'success' || needs.test-core.result == 'skipped' || (needs.test-core.result == 'failure' && needs.approve-flaky-test-core.result == 'success')) && (needs.test-proxy-e2e.result == 'success' || needs.test-proxy-e2e.result == 'skipped') && (needs.test-memory.result == 'success' || needs.test-memory.result == 'skipped' || (needs.test-memory.result == 'failure' && needs.approve-memory-regression.result == 'success')) && (needs.test-core-integrations.result == 'success' || needs.test-core-integrations.result == 'skipped') && (needs.test-core-unit.result == 'success' || needs.test-core-unit.result == 'skipped') && (needs.test-cli-harness.result == 'success' || needs.test-cli-harness.result == 'skipped') && (needs.test-vllm.result == 'success' || needs.test-vllm.result == 'skipped') && (needs.resolve-cli-versions.result == 'success' || needs.resolve-cli-versions.result == 'skipped') && (needs.test-framework.result == 'success' || needs.test-framework.result == 'skipped') && (needs.test-plugins.result == 'success' || needs.test-plugins.result == 'skipped') && (needs.test-bifrost-http.result == 'success' || needs.test-bifrost-http.result == 'skipped') && (needs.test-live.result == 'success' || needs.test-live.result == 'skipped') && (needs.test-migrations.result == 'success' || needs.test-migrations.result == 'skipped') && (needs.test-agentgateway.result == 'success' || needs.test-agentgateway.result == 'skipped') && (needs.test-api-integrations.result == 'success' || needs.test-api-integrations.result == 'skipped') && (needs.test-cost-accuracy.result == 'success' || needs.test-cost-accuracy.result == 'skipped') && (needs.test-docker-image-amd64.result == 'success' || needs.test-docker-image-amd64.result == 'skipped') && (needs.test-docker-image-arm64.result == 'success' || needs.test-docker-image-arm64.result == 'skipped')" runs-on: ubuntu-latest permissions: contents: write outputs: success: ${{ steps.release.outputs.success }} version: ${{ needs.detect-changes.outputs.core-version }} steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > api.github.com:443 github.com:443 nodejs.org:443 release-assets.githubusercontent.com:443 - 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.27.3" - name: Set up Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "25" - name: Configure Git run: | git config user.name "GitHub Actions Bot" git config user.email "github-actions[bot]@users.noreply.github.com" - name: Release core id: release env: GH_TOKEN: ${{ secrets.GH_TOKEN }} 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 }} AZURE_API_VERSION: ${{ secrets.AZURE_API_VERSION }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} 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 }} ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }} PERPLEXITY_API_KEY: ${{ secrets.PERPLEXITY_API_KEY }} SGL_API_KEY: ${{ secrets.SGL_API_KEY }} CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }} MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }} SARVAM_API_KEY: ${{ secrets.SARVAM_API_KEY }} TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }} DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} DATABRICKS_WORKSPACE_URL: ${{ secrets.DATABRICKS_WORKSPACE_URL }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 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 }} VERTEX_GCS_BUCKET: ${{ secrets.VERTEX_GCS_BUCKET }} VERTEX_GCS_PREFIX: ${{ secrets.VERTEX_GCS_PREFIX }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} HUGGING_FACE_API_KEY: ${{ secrets.HUGGING_FACE_API_KEY }} AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} AWS_BEDROCK_ROLE_ARN: ${{ secrets.AWS_BEDROCK_ROLE_ARN }} REPLICATE_API_KEY: ${{ secrets.REPLICATE_API_KEY }} REPLICATE_OWNER: ${{ secrets.REPLICATE_OWNER }} RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }} RUNWARE_API_KEY: ${{ secrets.RUNWARE_API_KEY }} run: ./.github/workflows/scripts/release-core.sh "${{ needs.detect-changes.outputs.core-version }}" framework-release: needs: [ check-skip, detect-changes, test-core, test-core-integrations, test-core-unit, approve-flaky-test-core, test-proxy-e2e, resolve-cli-versions, test-cli-harness, test-framework, test-plugins, test-bifrost-http, test-live, test-migrations, test-agentgateway, test-api-integrations, test-cost-accuracy, test-docker-image-amd64, test-docker-image-arm64, core-release, ] if: "always() && needs.check-skip.outputs.should-skip != 'true' && needs.detect-changes.result == 'success' && needs.detect-changes.outputs.framework-needs-release == 'true' && (needs.test-core.result == 'success' || needs.test-core.result == 'skipped' || (needs.test-core.result == 'failure' && needs.approve-flaky-test-core.result == 'success')) && (needs.test-proxy-e2e.result == 'success' || needs.test-proxy-e2e.result == 'skipped') && (needs.test-core-integrations.result == 'success' || needs.test-core-integrations.result == 'skipped') && (needs.test-core-unit.result == 'success' || needs.test-core-unit.result == 'skipped') && (needs.test-cli-harness.result == 'success' || needs.test-cli-harness.result == 'skipped') && (needs.resolve-cli-versions.result == 'success' || needs.resolve-cli-versions.result == 'skipped') && (needs.test-framework.result == 'success' || needs.test-framework.result == 'skipped') && (needs.test-plugins.result == 'success' || needs.test-plugins.result == 'skipped') && (needs.test-bifrost-http.result == 'success' || needs.test-bifrost-http.result == 'skipped') && (needs.test-live.result == 'success' || needs.test-live.result == 'skipped') && (needs.test-migrations.result == 'success' || needs.test-migrations.result == 'skipped') && (needs.test-agentgateway.result == 'success' || needs.test-agentgateway.result == 'skipped') && (needs.test-api-integrations.result == 'success' || needs.test-api-integrations.result == 'skipped') && (needs.test-cost-accuracy.result == 'success' || needs.test-cost-accuracy.result == 'skipped') && (needs.test-docker-image-amd64.result == 'success' || needs.test-docker-image-amd64.result == 'skipped') && (needs.test-docker-image-arm64.result == 'success' || needs.test-docker-image-arm64.result == 'skipped') && (needs.detect-changes.outputs.core-needs-release == 'false' || needs.core-release.result == 'success' || needs.core-release.result == 'skipped')" runs-on: ubuntu-latest permissions: contents: write outputs: success: ${{ steps.release.outputs.success }} version: ${{ needs.detect-changes.outputs.framework-version }} steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > api.github.com:443 github.com:443 proxy.golang.org:443 release-assets.githubusercontent.com:443 storage.googleapis.com:443 sum.golang.org:443 - 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.27.3" - name: Configure Git run: | git config user.name "GitHub Actions Bot" git config user.email "github-actions[bot]@users.noreply.github.com" - name: Set up Docker Compose run: | # Verify Docker is available docker --version # Install Docker Compose if not available as plugin if ! docker compose version >/dev/null 2>&1; then echo "Installing Docker Compose..." ./.github/workflows/scripts/install-docker-compose.sh /usr/local/bin/docker-compose docker-compose --version else echo "Docker Compose plugin is available" docker compose version fi - name: Release framework id: release env: GH_TOKEN: ${{ secrets.GH_TOKEN }} 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 }} AZURE_API_VERSION: ${{ secrets.AZURE_API_VERSION }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} 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 }} ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }} PERPLEXITY_API_KEY: ${{ secrets.PERPLEXITY_API_KEY }} SGL_API_KEY: ${{ secrets.SGL_API_KEY }} CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }} MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }} SARVAM_API_KEY: ${{ secrets.SARVAM_API_KEY }} TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }} DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} DATABRICKS_WORKSPACE_URL: ${{ secrets.DATABRICKS_WORKSPACE_URL }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 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 }} VERTEX_GCS_BUCKET: ${{ secrets.VERTEX_GCS_BUCKET }} VERTEX_GCS_PREFIX: ${{ secrets.VERTEX_GCS_PREFIX }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 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 }} RUNWARE_API_KEY: ${{ secrets.RUNWARE_API_KEY }} run: ./.github/workflows/scripts/release-framework.sh "${{ needs.detect-changes.outputs.framework-version }}" plugins-release: needs: [ check-skip, detect-changes, test-core, test-core-integrations, test-core-unit, approve-flaky-test-core, test-proxy-e2e, resolve-cli-versions, test-cli-harness, test-framework, test-plugins, test-bifrost-http, test-live, test-migrations, test-agentgateway, test-api-integrations, test-cost-accuracy, test-docker-image-amd64, test-docker-image-arm64, core-release, framework-release, ] if: "always() && needs.check-skip.outputs.should-skip != 'true' && needs.detect-changes.result == 'success' && needs.detect-changes.outputs.plugins-need-release == 'true' && (needs.test-core.result == 'success' || needs.test-core.result == 'skipped' || (needs.test-core.result == 'failure' && needs.approve-flaky-test-core.result == 'success')) && (needs.test-proxy-e2e.result == 'success' || needs.test-proxy-e2e.result == 'skipped') && (needs.test-core-integrations.result == 'success' || needs.test-core-integrations.result == 'skipped') && (needs.test-core-unit.result == 'success' || needs.test-core-unit.result == 'skipped') && (needs.test-cli-harness.result == 'success' || needs.test-cli-harness.result == 'skipped') && (needs.resolve-cli-versions.result == 'success' || needs.resolve-cli-versions.result == 'skipped') && (needs.test-framework.result == 'success' || needs.test-framework.result == 'skipped') && (needs.test-plugins.result == 'success' || needs.test-plugins.result == 'skipped') && (needs.test-bifrost-http.result == 'success' || needs.test-bifrost-http.result == 'skipped') && (needs.test-live.result == 'success' || needs.test-live.result == 'skipped') && (needs.test-migrations.result == 'success' || needs.test-migrations.result == 'skipped') && (needs.test-agentgateway.result == 'success' || needs.test-agentgateway.result == 'skipped') && (needs.test-api-integrations.result == 'success' || needs.test-api-integrations.result == 'skipped') && (needs.test-cost-accuracy.result == 'success' || needs.test-cost-accuracy.result == 'skipped') && (needs.test-docker-image-amd64.result == 'success' || needs.test-docker-image-amd64.result == 'skipped') && (needs.test-docker-image-arm64.result == 'success' || needs.test-docker-image-arm64.result == 'skipped') && (needs.detect-changes.outputs.core-needs-release == 'false' || needs.core-release.result == 'success' || needs.core-release.result == 'skipped') && (needs.detect-changes.outputs.framework-needs-release == 'false' || needs.framework-release.result == 'success' || needs.framework-release.result == 'skipped')" runs-on: ubuntu-latest permissions: contents: write outputs: success: ${{ steps.release.outputs.success }} steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > _http._tcp.azure.archive.ubuntu.com:443 _https._tcp.esm.ubuntu.com:443 _https._tcp.motd.ubuntu.com:443 _https._tcp.packages.microsoft.com:443 api.github.com:443 azure.archive.ubuntu.com:80 esm.ubuntu.com:443 github.com:443 nodejs.org:443 packages.microsoft.com:443 proxy.golang.org:443 release-assets.githubusercontent.com:443 storage.googleapis.com:443 sum.golang.org:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 fetch-tags: true token: ${{ secrets.GH_TOKEN }} - name: Install jq run: | sudo apt-get update sudo apt-get install -y jq - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.27.3" - name: Set up Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "25" - name: Configure Git run: | git config user.name "GitHub Actions Bot" git config user.email "github-actions[bot]@users.noreply.github.com" - name: Set up Docker Compose run: | # Verify Docker is available docker --version # Install Docker Compose if not available as plugin if ! docker compose version >/dev/null 2>&1; then echo "Installing Docker Compose..." ./.github/workflows/scripts/install-docker-compose.sh /usr/local/bin/docker-compose docker-compose --version else echo "Docker Compose plugin is available" docker compose version fi - name: Release all changed plugins id: release env: GH_TOKEN: ${{ secrets.GH_TOKEN }} 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 }} AZURE_API_VERSION: ${{ secrets.AZURE_API_VERSION }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} 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 }} ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }} PERPLEXITY_API_KEY: ${{ secrets.PERPLEXITY_API_KEY }} SGL_API_KEY: ${{ secrets.SGL_API_KEY }} CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }} MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }} SARVAM_API_KEY: ${{ secrets.SARVAM_API_KEY }} TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }} DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} DATABRICKS_WORKSPACE_URL: ${{ secrets.DATABRICKS_WORKSPACE_URL }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 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 }} VERTEX_GCS_BUCKET: ${{ secrets.VERTEX_GCS_BUCKET }} VERTEX_GCS_PREFIX: ${{ secrets.VERTEX_GCS_PREFIX }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 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 }} RUNWARE_API_KEY: ${{ secrets.RUNWARE_API_KEY }} run: ./.github/workflows/scripts/release-all-plugins.sh '${{ needs.detect-changes.outputs.changed-plugins }}' # Prep: update dependencies, validate build, commit/push bifrost-http-prep: needs: [ check-skip, detect-changes, test-core, test-core-integrations, test-core-unit, approve-flaky-test-core, test-proxy-e2e, test-memory, approve-memory-regression, resolve-cli-versions, test-cli-harness, test-framework, test-plugins, test-bifrost-http, test-live, test-migrations, test-agentgateway, test-api-integrations, test-cost-accuracy, test-docker-image-amd64, test-docker-image-arm64, core-release, framework-release, plugins-release, ] if: "always() && needs.check-skip.outputs.should-skip != 'true' && needs.detect-changes.result == 'success' && needs.detect-changes.outputs.bifrost-http-needs-release == 'true' && (needs.test-core.result == 'success' || needs.test-core.result == 'skipped' || (needs.test-core.result == 'failure' && needs.approve-flaky-test-core.result == 'success')) && (needs.test-proxy-e2e.result == 'success' || needs.test-proxy-e2e.result == 'skipped') && (needs.test-memory.result == 'success' || needs.test-memory.result == 'skipped' || (needs.test-memory.result == 'failure' && needs.approve-memory-regression.result == 'success')) && (needs.test-core-integrations.result == 'success' || needs.test-core-integrations.result == 'skipped') && (needs.test-core-unit.result == 'success' || needs.test-core-unit.result == 'skipped') && (needs.test-cli-harness.result == 'success' || needs.test-cli-harness.result == 'skipped') && (needs.resolve-cli-versions.result == 'success' || needs.resolve-cli-versions.result == 'skipped') && (needs.test-framework.result == 'success' || needs.test-framework.result == 'skipped') && (needs.test-plugins.result == 'success' || needs.test-plugins.result == 'skipped') && (needs.test-bifrost-http.result == 'success' || needs.test-bifrost-http.result == 'skipped') && (needs.test-live.result == 'success' || needs.test-live.result == 'skipped') && (needs.test-migrations.result == 'success' || needs.test-migrations.result == 'skipped') && (needs.test-agentgateway.result == 'success' || needs.test-agentgateway.result == 'skipped') && (needs.test-api-integrations.result == 'success' || needs.test-api-integrations.result == 'skipped') && (needs.test-cost-accuracy.result == 'success' || needs.test-cost-accuracy.result == 'skipped') && (needs.test-docker-image-amd64.result == 'success' || needs.test-docker-image-amd64.result == 'skipped') && (needs.test-docker-image-arm64.result == 'success' || needs.test-docker-image-arm64.result == 'skipped') && (needs.detect-changes.outputs.core-needs-release == 'false' || needs.core-release.result == 'success' || needs.core-release.result == 'skipped') && (needs.detect-changes.outputs.framework-needs-release == 'false' || needs.framework-release.result == 'success' || needs.framework-release.result == 'skipped') && (needs.detect-changes.outputs.plugins-need-release == 'false' || needs.plugins-release.result == 'success' || needs.plugins-release.result == 'skipped')" runs-on: ubuntu-latest permissions: contents: write outputs: success: ${{ steps.prep.outputs.success }} steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > api.github.com:443 fonts.googleapis.com:443 fonts.gstatic.com:443 github.com:443 nodejs.org:443 proxy.golang.org:443 registry.npmjs.org:443 release-assets.githubusercontent.com:443 storage.googleapis.com:443 sum.golang.org:443 - 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.27.3" - name: Set up Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "25" - name: Configure Git run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - name: Prep bifrost-http release id: prep env: GH_TOKEN: ${{ secrets.GH_TOKEN }} run: ./.github/workflows/scripts/release-bifrost-http-prep.sh "${{ needs.detect-changes.outputs.transport-version }}" # Build darwin, windows, linux/amd64 binaries and upload to R2 build-binaries-x86: needs: [bifrost-http-prep, detect-changes] if: "always() && needs.bifrost-http-prep.result == 'success' && needs.bifrost-http-prep.outputs.success == 'true'" runs-on: ubuntu-latest permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > 7defe2860d5ee49a1e667e1eeea34b25.r2.cloudflarestorage.com:443 _http._tcp.azure.archive.ubuntu.com:443 _https._tcp.esm.ubuntu.com:443 _https._tcp.motd.ubuntu.com:443 _https._tcp.packages.microsoft.com:443 api.github.com:443 azure.archive.ubuntu.com:80 esm.ubuntu.com:443 files.pythonhosted.org:443 fonts.googleapis.com:443 fonts.gstatic.com:443 github.com:443 nodejs.org:443 packages.microsoft.com:443 proxy.golang.org:443 pypi.org:443 registry.npmjs.org:443 release-assets.githubusercontent.com:443 storage.googleapis.com:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 fetch-tags: true token: ${{ secrets.GH_TOKEN }} - name: Pull latest changes run: git pull origin ${{ github.ref_name }} - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.27.3" - name: Set up Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "25" - name: Build UI run: make build-ui - name: Install cross-compilation toolchains run: bash ./.github/workflows/scripts/install-cross-compilers.sh - name: Build executables run: bash ./.github/workflows/scripts/build-executables.sh "${{ needs.detect-changes.outputs.transport-version }}" "darwin/amd64 darwin/arm64 linux/amd64 windows/amd64" - name: Configure R2 env: R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }} R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} run: bash ./.github/workflows/scripts/configure-r2.sh - name: Upload to R2 env: R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }} R2_BUCKET: ${{ secrets.R2_BUCKET }} SKIP_LATEST_UPLOAD: "true" run: bash ./.github/workflows/scripts/upload-to-r2.sh "transports/v${{ needs.detect-changes.outputs.transport-version }}" # Build linux/arm64 natively on ARM runner and upload to R2 build-binaries-arm64: needs: [bifrost-http-prep, detect-changes] if: "always() && needs.bifrost-http-prep.result == 'success' && needs.bifrost-http-prep.outputs.success == 'true'" runs-on: ubuntu-24.04-arm permissions: contents: read steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > 7defe2860d5ee49a1e667e1eeea34b25.r2.cloudflarestorage.com:443 api.github.com:443 files.pythonhosted.org:443 fonts.googleapis.com:443 fonts.gstatic.com:443 github.com:443 nodejs.org:443 proxy.golang.org:443 pypi.org:443 registry.npmjs.org:443 release-assets.githubusercontent.com:443 storage.googleapis.com:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 fetch-tags: true token: ${{ secrets.GH_TOKEN }} - name: Pull latest changes run: git pull origin ${{ github.ref_name }} - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.27.3" - name: Set up Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "25" - name: Build UI run: make build-ui - name: Build linux/arm64 binary run: bash ./.github/workflows/scripts/build-executables.sh "${{ needs.detect-changes.outputs.transport-version }}" "linux/arm64" - name: Configure R2 env: R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }} R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} run: bash ./.github/workflows/scripts/configure-r2.sh - name: Upload to R2 env: R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }} R2_BUCKET: ${{ secrets.R2_BUCKET }} SKIP_LATEST_UPLOAD: "true" run: bash ./.github/workflows/scripts/upload-to-r2.sh "transports/v${{ needs.detect-changes.outputs.transport-version }}" # Finalize: changelog, tagging, GitHub release, R2 latest copy bifrost-http-release: needs: [build-binaries-x86, build-binaries-arm64, detect-changes, bifrost-http-prep] if: "always() && needs.build-binaries-x86.result == 'success' && needs.build-binaries-arm64.result == 'success'" runs-on: ubuntu-latest permissions: contents: write outputs: success: ${{ steps.release.outputs.success }} version: ${{ needs.detect-changes.outputs.transport-version }} steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > 7defe2860d5ee49a1e667e1eeea34b25.r2.cloudflarestorage.com:443 api.github.com:443 files.pythonhosted.org:443 github.com:443 pypi.org:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 fetch-tags: true token: ${{ secrets.GH_TOKEN }} - name: Pull latest changes run: git pull origin ${{ github.ref_name }} - name: Configure R2 env: R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }} R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} run: bash ./.github/workflows/scripts/configure-r2.sh - name: Finalize bifrost-http release id: release env: GH_TOKEN: ${{ secrets.GH_TOKEN }} R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }} R2_BUCKET: ${{ secrets.R2_BUCKET }} run: ./.github/workflows/scripts/release-bifrost-http-finalize.sh "${{ needs.detect-changes.outputs.transport-version }}" # Docker build amd64 docker-build-amd64: needs: [ check-skip, detect-changes, test-core, test-core-integrations, test-core-unit, approve-flaky-test-core, test-proxy-e2e, test-memory, approve-memory-regression, resolve-cli-versions, test-cli-harness, test-framework, test-plugins, test-bifrost-http, test-live, test-migrations, test-agentgateway, test-api-integrations, test-cost-accuracy, test-docker-image-amd64, test-docker-image-arm64, core-release, framework-release, plugins-release, bifrost-http-release, ] if: "always() && needs.check-skip.outputs.should-skip != 'true' && needs.detect-changes.result == 'success' && needs.detect-changes.outputs.docker-needs-release == 'true' && (needs.test-core.result == 'success' || needs.test-core.result == 'skipped' || (needs.test-core.result == 'failure' && needs.approve-flaky-test-core.result == 'success')) && (needs.test-proxy-e2e.result == 'success' || needs.test-proxy-e2e.result == 'skipped') && (needs.test-memory.result == 'success' || needs.test-memory.result == 'skipped' || (needs.test-memory.result == 'failure' && needs.approve-memory-regression.result == 'success')) && (needs.test-core-integrations.result == 'success' || needs.test-core-integrations.result == 'skipped') && (needs.test-core-unit.result == 'success' || needs.test-core-unit.result == 'skipped') && (needs.test-cli-harness.result == 'success' || needs.test-cli-harness.result == 'skipped') && (needs.resolve-cli-versions.result == 'success' || needs.resolve-cli-versions.result == 'skipped') && (needs.test-framework.result == 'success' || needs.test-framework.result == 'skipped') && (needs.test-plugins.result == 'success' || needs.test-plugins.result == 'skipped') && (needs.test-bifrost-http.result == 'success' || needs.test-bifrost-http.result == 'skipped') && (needs.test-live.result == 'success' || needs.test-live.result == 'skipped') && (needs.test-migrations.result == 'success' || needs.test-migrations.result == 'skipped') && (needs.test-agentgateway.result == 'success' || needs.test-agentgateway.result == 'skipped') && (needs.test-api-integrations.result == 'success' || needs.test-api-integrations.result == 'skipped') && (needs.test-cost-accuracy.result == 'success' || needs.test-cost-accuracy.result == 'skipped') && (needs.test-docker-image-amd64.result == 'success' || needs.test-docker-image-amd64.result == 'skipped') && (needs.test-docker-image-arm64.result == 'success' || needs.test-docker-image-arm64.result == 'skipped') && (needs.detect-changes.outputs.core-needs-release == 'false' || needs.core-release.result == 'success' || needs.core-release.result == 'skipped') && (needs.detect-changes.outputs.framework-needs-release == 'false' || needs.framework-release.result == 'success' || needs.framework-release.result == 'skipped') && (needs.detect-changes.outputs.plugins-need-release == 'false' || needs.plugins-release.result == 'success' || needs.plugins-release.result == 'skipped') && (needs.detect-changes.outputs.bifrost-http-needs-release == 'false' || needs.bifrost-http-release.result == 'success' || needs.bifrost-http-release.result == 'skipped')" runs-on: ubuntu-latest permissions: contents: write packages: write env: REGISTRY: docker.io ACCOUNT: maximhq IMAGE_NAME: bifrost steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > auth.docker.io:443 dl-cdn.alpinelinux.org:443 fonts.googleapis.com:443 fonts.gstatic.com:443 ghcr.io:443 github.com:443 pkg-containers.githubusercontent.com:443 production.cloudflare.docker.com:443 production.cloudfront.docker.com:443 proxy.golang.org:443 registry-1.docker.io:443 registry.npmjs.org:443 storage.googleapis.com:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 fetch-tags: true - name: Verify bifrost-http release id: verify continue-on-error: true env: GH_TOKEN: ${{ secrets.GH_TOKEN }} run: | ./.github/workflows/scripts/verify-bifrost-http-release.sh "${{ needs.detect-changes.outputs.transport-version }}" "${{ needs.detect-changes.outputs.bifrost-http-needs-release }}" echo "verified=true" >> $GITHUB_OUTPUT - name: Setup Docker Buidx uses: step-security/setup-buildx-action@f931205d68723ad9589fd2a7e2ece238bf9de341 # v4.0.0 - name: Log in to Docker Hub uses: step-security/docker-login-action@870af644803bf9f204aed474adbad2958fec048b # v4.1.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to GitHub Container Registry uses: step-security/docker-login-action@870af644803bf9f204aed474adbad2958fec048b # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Determine Docker tags id: tags run: | git pull origin ${{ github.ref_name }} VERSION="${{ needs.detect-changes.outputs.transport-version }}" BASE_TAG="${{ env.REGISTRY }}/${{ env.ACCOUNT }}/${{ env.IMAGE_NAME }}:v${VERSION}-amd64" GHCR_REPO=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') GHCR_TAG="ghcr.io/${GHCR_REPO}:v${VERSION}-amd64" echo "tags=${BASE_TAG}" >> $GITHUB_OUTPUT echo "dockerhub-tag=${BASE_TAG}" >> $GITHUB_OUTPUT echo "ghcr-tag=${GHCR_TAG}" >> $GITHUB_OUTPUT - name: Build and push AMD64 Docker image uses: step-security/docker-build-push-action@846549baaf047e867d038826129a64d81df0f704 # v7.1.0 with: context: . build-args: | VERSION=${{ needs.detect-changes.outputs.transport-version }} file: ./transports/Dockerfile push: true tags: ${{ steps.tags.outputs.tags }} platforms: linux/amd64 # Mirror the just-pushed Docker Hub image to GitHub Container Registry (ghcr.io) # by copying it with buildx imagetools (no rebuild). Best-effort: a ghcr.io # failure must not fail the Docker Hub release. - name: Mirror AMD64 image to GitHub Container Registry continue-on-error: true run: | docker buildx imagetools create \ --tag "${{ steps.tags.outputs.ghcr-tag }}" \ "${{ steps.tags.outputs.dockerhub-tag }}" # Docker build arm64 docker-build-arm64: needs: [ check-skip, detect-changes, test-core, test-core-integrations, test-core-unit, approve-flaky-test-core, test-proxy-e2e, test-memory, approve-memory-regression, resolve-cli-versions, test-cli-harness, test-framework, test-plugins, test-bifrost-http, test-live, test-migrations, test-agentgateway, test-api-integrations, test-cost-accuracy, test-docker-image-amd64, test-docker-image-arm64, core-release, framework-release, plugins-release, bifrost-http-release, ] if: "always() && needs.check-skip.outputs.should-skip != 'true' && needs.detect-changes.result == 'success' && needs.detect-changes.outputs.docker-needs-release == 'true' && (needs.test-core.result == 'success' || needs.test-core.result == 'skipped' || (needs.test-core.result == 'failure' && needs.approve-flaky-test-core.result == 'success')) && (needs.test-proxy-e2e.result == 'success' || needs.test-proxy-e2e.result == 'skipped') && (needs.test-memory.result == 'success' || needs.test-memory.result == 'skipped' || (needs.test-memory.result == 'failure' && needs.approve-memory-regression.result == 'success')) && (needs.test-core-integrations.result == 'success' || needs.test-core-integrations.result == 'skipped') && (needs.test-core-unit.result == 'success' || needs.test-core-unit.result == 'skipped') && (needs.test-cli-harness.result == 'success' || needs.test-cli-harness.result == 'skipped') && (needs.resolve-cli-versions.result == 'success' || needs.resolve-cli-versions.result == 'skipped') && (needs.test-framework.result == 'success' || needs.test-framework.result == 'skipped') && (needs.test-plugins.result == 'success' || needs.test-plugins.result == 'skipped') && (needs.test-bifrost-http.result == 'success' || needs.test-bifrost-http.result == 'skipped') && (needs.test-live.result == 'success' || needs.test-live.result == 'skipped') && (needs.test-migrations.result == 'success' || needs.test-migrations.result == 'skipped') && (needs.test-agentgateway.result == 'success' || needs.test-agentgateway.result == 'skipped') && (needs.test-api-integrations.result == 'success' || needs.test-api-integrations.result == 'skipped') && (needs.test-cost-accuracy.result == 'success' || needs.test-cost-accuracy.result == 'skipped') && (needs.test-docker-image-amd64.result == 'success' || needs.test-docker-image-amd64.result == 'skipped') && (needs.test-docker-image-arm64.result == 'success' || needs.test-docker-image-arm64.result == 'skipped') && (needs.detect-changes.outputs.core-needs-release == 'false' || needs.core-release.result == 'success' || needs.core-release.result == 'skipped') && (needs.detect-changes.outputs.framework-needs-release == 'false' || needs.framework-release.result == 'success' || needs.framework-release.result == 'skipped') && (needs.detect-changes.outputs.plugins-need-release == 'false' || needs.plugins-release.result == 'success' || needs.plugins-release.result == 'skipped') && (needs.detect-changes.outputs.bifrost-http-needs-release == 'false' || needs.bifrost-http-release.result == 'success' || needs.bifrost-http-release.result == 'skipped')" runs-on: ubuntu-24.04-arm permissions: contents: write packages: write env: REGISTRY: docker.io ACCOUNT: maximhq IMAGE_NAME: bifrost steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > auth.docker.io:443 dl-cdn.alpinelinux.org:443 fonts.googleapis.com:443 fonts.gstatic.com:443 ghcr.io:443 github.com:443 pkg-containers.githubusercontent.com:443 production.cloudflare.docker.com:443 production.cloudfront.docker.com:443 proxy.golang.org:443 registry-1.docker.io:443 registry.npmjs.org:443 storage.googleapis.com:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 fetch-tags: true - name: Verify bifrost-http release id: verify continue-on-error: true env: GH_TOKEN: ${{ secrets.GH_TOKEN }} run: | ./.github/workflows/scripts/verify-bifrost-http-release.sh "${{ needs.detect-changes.outputs.transport-version }}" "${{ needs.detect-changes.outputs.bifrost-http-needs-release }}" echo "verified=true" >> $GITHUB_OUTPUT - name: Setup Docker Buidx uses: step-security/setup-buildx-action@f931205d68723ad9589fd2a7e2ece238bf9de341 # v4.0.0 - name: Log in to Docker Hub uses: step-security/docker-login-action@870af644803bf9f204aed474adbad2958fec048b # v4.1.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to GitHub Container Registry uses: step-security/docker-login-action@870af644803bf9f204aed474adbad2958fec048b # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Determine Docker tags id: tags run: | git pull origin ${{ github.ref_name }} VERSION="${{ needs.detect-changes.outputs.transport-version }}" BASE_TAG="${{ env.REGISTRY }}/${{ env.ACCOUNT }}/${{ env.IMAGE_NAME }}:v${VERSION}-arm64" GHCR_REPO=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') GHCR_TAG="ghcr.io/${GHCR_REPO}:v${VERSION}-arm64" echo "tags=${BASE_TAG}" >> $GITHUB_OUTPUT echo "dockerhub-tag=${BASE_TAG}" >> $GITHUB_OUTPUT echo "ghcr-tag=${GHCR_TAG}" >> $GITHUB_OUTPUT - name: Build and push ARM64 Docker image uses: step-security/docker-build-push-action@846549baaf047e867d038826129a64d81df0f704 # v7.1.0 with: context: . file: ./transports/Dockerfile push: true build-args: | VERSION=${{ needs.detect-changes.outputs.transport-version }} tags: ${{ steps.tags.outputs.tags }} platforms: linux/arm64 # Mirror the just-pushed Docker Hub image to GitHub Container Registry (ghcr.io) # by copying it with buildx imagetools (no rebuild). Best-effort: a ghcr.io # failure must not fail the Docker Hub release. - name: Mirror ARM64 image to GitHub Container Registry continue-on-error: true run: | docker buildx imagetools create \ --tag "${{ steps.tags.outputs.ghcr-tag }}" \ "${{ steps.tags.outputs.dockerhub-tag }}" # Docker manifest docker-manifest: needs: [check-skip, detect-changes, docker-build-amd64, docker-build-arm64] if: "always() && needs.check-skip.outputs.should-skip != 'true' && needs.docker-build-amd64.result == 'success' && needs.docker-build-arm64.result == 'success'" runs-on: ubuntu-latest permissions: contents: read packages: write env: REGISTRY: docker.io ACCOUNT: maximhq IMAGE_NAME: bifrost steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > auth.docker.io:443 ghcr.io:443 github.com:443 pkg-containers.githubusercontent.com:443 production.cloudflare.docker.com:443 production.cloudfront.docker.com:443 registry-1.docker.io:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Log in to Docker Hub uses: step-security/docker-login-action@870af644803bf9f204aed474adbad2958fec048b # v4.1.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to GitHub Container Registry uses: step-security/docker-login-action@870af644803bf9f204aed474adbad2958fec048b # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Create and push multi-arch manifest run: | ./.github/workflows/scripts/create-docker-manifest.sh "${{ needs.detect-changes.outputs.transport-version }}" # Docker build UBI9 amd64 docker-build-ubi9-amd64: needs: [ check-skip, detect-changes, test-core, test-core-integrations, test-core-unit, approve-flaky-test-core, test-proxy-e2e, test-memory, approve-memory-regression, resolve-cli-versions, test-cli-harness, test-framework, test-plugins, test-bifrost-http, test-live, test-migrations, test-agentgateway, test-api-integrations, test-cost-accuracy, test-docker-image-amd64, test-docker-image-arm64, core-release, framework-release, plugins-release, bifrost-http-release, ] if: "always() && needs.check-skip.outputs.should-skip != 'true' && needs.detect-changes.result == 'success' && needs.detect-changes.outputs.docker-needs-release == 'true' && (needs.test-core.result == 'success' || needs.test-core.result == 'skipped' || (needs.test-core.result == 'failure' && needs.approve-flaky-test-core.result == 'success')) && (needs.test-proxy-e2e.result == 'success' || needs.test-proxy-e2e.result == 'skipped') && (needs.test-memory.result == 'success' || needs.test-memory.result == 'skipped' || (needs.test-memory.result == 'failure' && needs.approve-memory-regression.result == 'success')) && (needs.test-core-integrations.result == 'success' || needs.test-core-integrations.result == 'skipped') && (needs.test-core-unit.result == 'success' || needs.test-core-unit.result == 'skipped') && (needs.test-cli-harness.result == 'success' || needs.test-cli-harness.result == 'skipped') && (needs.resolve-cli-versions.result == 'success' || needs.resolve-cli-versions.result == 'skipped') && (needs.test-framework.result == 'success' || needs.test-framework.result == 'skipped') && (needs.test-plugins.result == 'success' || needs.test-plugins.result == 'skipped') && (needs.test-bifrost-http.result == 'success' || needs.test-bifrost-http.result == 'skipped') && (needs.test-live.result == 'success' || needs.test-live.result == 'skipped') && (needs.test-migrations.result == 'success' || needs.test-migrations.result == 'skipped') && (needs.test-agentgateway.result == 'success' || needs.test-agentgateway.result == 'skipped') && (needs.test-api-integrations.result == 'success' || needs.test-api-integrations.result == 'skipped') && (needs.test-cost-accuracy.result == 'success' || needs.test-cost-accuracy.result == 'skipped') && (needs.test-docker-image-amd64.result == 'success' || needs.test-docker-image-amd64.result == 'skipped') && (needs.test-docker-image-arm64.result == 'success' || needs.test-docker-image-arm64.result == 'skipped') && (needs.detect-changes.outputs.core-needs-release == 'false' || needs.core-release.result == 'success' || needs.core-release.result == 'skipped') && (needs.detect-changes.outputs.framework-needs-release == 'false' || needs.framework-release.result == 'success' || needs.framework-release.result == 'skipped') && (needs.detect-changes.outputs.plugins-need-release == 'false' || needs.plugins-release.result == 'success' || needs.plugins-release.result == 'skipped') && (needs.detect-changes.outputs.bifrost-http-needs-release == 'false' || needs.bifrost-http-release.result == 'success' || needs.bifrost-http-release.result == 'skipped')" runs-on: ubuntu-latest permissions: contents: write packages: write env: REGISTRY: docker.io ACCOUNT: maximhq IMAGE_NAME: bifrost steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > auth.docker.io:443 dl-cdn.alpinelinux.org:443 fonts.googleapis.com:443 fonts.gstatic.com:443 ghcr.io:443 github.com:443 pkg-containers.githubusercontent.com:443 production.cloudflare.docker.com:443 production.cloudfront.docker.com:443 proxy.golang.org:443 registry-1.docker.io:443 *.redhat.com:443 *.quay.io:443 registry.npmjs.org:443 storage.googleapis.com:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 fetch-tags: true - name: Verify bifrost-http release id: verify continue-on-error: true env: GH_TOKEN: ${{ secrets.GH_TOKEN }} run: | ./.github/workflows/scripts/verify-bifrost-http-release.sh "${{ needs.detect-changes.outputs.transport-version }}" "${{ needs.detect-changes.outputs.bifrost-http-needs-release }}" echo "verified=true" >> $GITHUB_OUTPUT - name: Setup Docker Buildx uses: step-security/setup-buildx-action@f931205d68723ad9589fd2a7e2ece238bf9de341 # v4.0.0 - name: Log in to Docker Hub uses: step-security/docker-login-action@870af644803bf9f204aed474adbad2958fec048b # v4.1.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to GitHub Container Registry uses: step-security/docker-login-action@870af644803bf9f204aed474adbad2958fec048b # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Determine Docker tags id: tags run: | git pull origin ${{ github.ref_name }} VERSION="${{ needs.detect-changes.outputs.transport-version }}" BASE_TAG="${{ env.REGISTRY }}/${{ env.ACCOUNT }}/${{ env.IMAGE_NAME }}:v${VERSION}-ubi9-amd64" GHCR_REPO=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') GHCR_TAG="ghcr.io/${GHCR_REPO}:v${VERSION}-ubi9-amd64" echo "tags=${BASE_TAG}" >> $GITHUB_OUTPUT echo "dockerhub-tag=${BASE_TAG}" >> $GITHUB_OUTPUT echo "ghcr-tag=${GHCR_TAG}" >> $GITHUB_OUTPUT - name: Build and push UBI9 AMD64 Docker image uses: step-security/docker-build-push-action@846549baaf047e867d038826129a64d81df0f704 # v7.1.0 with: context: . build-args: | VERSION=${{ needs.detect-changes.outputs.transport-version }} file: ./transports/Dockerfile.redhat push: true tags: ${{ steps.tags.outputs.tags }} platforms: linux/amd64 cache-from: type=gha,scope=docker-ubi9-amd64 cache-to: type=gha,mode=max,scope=docker-ubi9-amd64 # Mirror the just-pushed Docker Hub image to GitHub Container Registry (ghcr.io) # by copying it with buildx imagetools (no rebuild). Best-effort: a ghcr.io # failure must not fail the Docker Hub release. - name: Mirror UBI9 AMD64 image to GitHub Container Registry continue-on-error: true run: | docker buildx imagetools create \ --tag "${{ steps.tags.outputs.ghcr-tag }}" \ "${{ steps.tags.outputs.dockerhub-tag }}" # Docker build UBI9 arm64 docker-build-ubi9-arm64: needs: [ check-skip, detect-changes, test-core, test-core-integrations, test-core-unit, approve-flaky-test-core, test-proxy-e2e, test-memory, approve-memory-regression, resolve-cli-versions, test-cli-harness, test-framework, test-plugins, test-bifrost-http, test-live, test-migrations, test-agentgateway, test-api-integrations, test-cost-accuracy, test-docker-image-amd64, test-docker-image-arm64, core-release, framework-release, plugins-release, bifrost-http-release, ] if: "always() && needs.check-skip.outputs.should-skip != 'true' && needs.detect-changes.result == 'success' && needs.detect-changes.outputs.docker-needs-release == 'true' && (needs.test-core.result == 'success' || needs.test-core.result == 'skipped' || (needs.test-core.result == 'failure' && needs.approve-flaky-test-core.result == 'success')) && (needs.test-proxy-e2e.result == 'success' || needs.test-proxy-e2e.result == 'skipped') && (needs.test-memory.result == 'success' || needs.test-memory.result == 'skipped' || (needs.test-memory.result == 'failure' && needs.approve-memory-regression.result == 'success')) && (needs.test-core-integrations.result == 'success' || needs.test-core-integrations.result == 'skipped') && (needs.test-core-unit.result == 'success' || needs.test-core-unit.result == 'skipped') && (needs.test-cli-harness.result == 'success' || needs.test-cli-harness.result == 'skipped') && (needs.resolve-cli-versions.result == 'success' || needs.resolve-cli-versions.result == 'skipped') && (needs.test-framework.result == 'success' || needs.test-framework.result == 'skipped') && (needs.test-plugins.result == 'success' || needs.test-plugins.result == 'skipped') && (needs.test-bifrost-http.result == 'success' || needs.test-bifrost-http.result == 'skipped') && (needs.test-live.result == 'success' || needs.test-live.result == 'skipped') && (needs.test-migrations.result == 'success' || needs.test-migrations.result == 'skipped') && (needs.test-agentgateway.result == 'success' || needs.test-agentgateway.result == 'skipped') && (needs.test-api-integrations.result == 'success' || needs.test-api-integrations.result == 'skipped') && (needs.test-cost-accuracy.result == 'success' || needs.test-cost-accuracy.result == 'skipped') && (needs.test-docker-image-amd64.result == 'success' || needs.test-docker-image-amd64.result == 'skipped') && (needs.test-docker-image-arm64.result == 'success' || needs.test-docker-image-arm64.result == 'skipped') && (needs.detect-changes.outputs.core-needs-release == 'false' || needs.core-release.result == 'success' || needs.core-release.result == 'skipped') && (needs.detect-changes.outputs.framework-needs-release == 'false' || needs.framework-release.result == 'success' || needs.framework-release.result == 'skipped') && (needs.detect-changes.outputs.plugins-need-release == 'false' || needs.plugins-release.result == 'success' || needs.plugins-release.result == 'skipped') && (needs.detect-changes.outputs.bifrost-http-needs-release == 'false' || needs.bifrost-http-release.result == 'success' || needs.bifrost-http-release.result == 'skipped')" runs-on: ubuntu-24.04-arm permissions: contents: write packages: write env: REGISTRY: docker.io ACCOUNT: maximhq IMAGE_NAME: bifrost steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > auth.docker.io:443 dl-cdn.alpinelinux.org:443 fonts.googleapis.com:443 fonts.gstatic.com:443 ghcr.io:443 github.com:443 pkg-containers.githubusercontent.com:443 production.cloudflare.docker.com:443 production.cloudfront.docker.com:443 proxy.golang.org:443 registry-1.docker.io:443 *.redhat.com:443 *.quay.io:443 registry.npmjs.org:443 storage.googleapis.com:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 fetch-tags: true - name: Verify bifrost-http release id: verify continue-on-error: true env: GH_TOKEN: ${{ secrets.GH_TOKEN }} run: | ./.github/workflows/scripts/verify-bifrost-http-release.sh "${{ needs.detect-changes.outputs.transport-version }}" "${{ needs.detect-changes.outputs.bifrost-http-needs-release }}" echo "verified=true" >> $GITHUB_OUTPUT - name: Setup Docker Buildx uses: step-security/setup-buildx-action@f931205d68723ad9589fd2a7e2ece238bf9de341 # v4.0.0 - name: Log in to Docker Hub uses: step-security/docker-login-action@870af644803bf9f204aed474adbad2958fec048b # v4.1.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to GitHub Container Registry uses: step-security/docker-login-action@870af644803bf9f204aed474adbad2958fec048b # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Determine Docker tags id: tags run: | git pull origin ${{ github.ref_name }} VERSION="${{ needs.detect-changes.outputs.transport-version }}" BASE_TAG="${{ env.REGISTRY }}/${{ env.ACCOUNT }}/${{ env.IMAGE_NAME }}:v${VERSION}-ubi9-arm64" GHCR_REPO=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') GHCR_TAG="ghcr.io/${GHCR_REPO}:v${VERSION}-ubi9-arm64" echo "tags=${BASE_TAG}" >> $GITHUB_OUTPUT echo "dockerhub-tag=${BASE_TAG}" >> $GITHUB_OUTPUT echo "ghcr-tag=${GHCR_TAG}" >> $GITHUB_OUTPUT - name: Build and push UBI9 ARM64 Docker image uses: step-security/docker-build-push-action@846549baaf047e867d038826129a64d81df0f704 # v7.1.0 with: context: . file: ./transports/Dockerfile.redhat push: true build-args: | VERSION=${{ needs.detect-changes.outputs.transport-version }} tags: ${{ steps.tags.outputs.tags }} platforms: linux/arm64 cache-from: type=gha,scope=docker-ubi9-arm64 cache-to: type=gha,mode=max,scope=docker-ubi9-arm64 # Mirror the just-pushed Docker Hub image to GitHub Container Registry (ghcr.io) # by copying it with buildx imagetools (no rebuild). Best-effort: a ghcr.io # failure must not fail the Docker Hub release. - name: Mirror UBI9 ARM64 image to GitHub Container Registry continue-on-error: true run: | docker buildx imagetools create \ --tag "${{ steps.tags.outputs.ghcr-tag }}" \ "${{ steps.tags.outputs.dockerhub-tag }}" # Docker UBI9 manifest — merges amd64 + arm64 into a single multi-arch tag docker-manifest-ubi9: needs: [check-skip, detect-changes, docker-build-ubi9-amd64, docker-build-ubi9-arm64] if: "always() && needs.check-skip.outputs.should-skip != 'true' && needs.docker-build-ubi9-amd64.result == 'success' && needs.docker-build-ubi9-arm64.result == 'success'" runs-on: ubuntu-latest permissions: contents: read packages: write env: REGISTRY: docker.io ACCOUNT: maximhq IMAGE_NAME: bifrost steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > auth.docker.io:443 ghcr.io:443 github.com:443 pkg-containers.githubusercontent.com:443 production.cloudflare.docker.com:443 production.cloudfront.docker.com:443 registry-1.docker.io:443 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Log in to Docker Hub uses: step-security/docker-login-action@870af644803bf9f204aed474adbad2958fec048b # v4.1.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to GitHub Container Registry uses: step-security/docker-login-action@870af644803bf9f204aed474adbad2958fec048b # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Create and push UBI9 multi-arch manifest run: | ./.github/workflows/scripts/create-docker-manifest-ubi9.sh "${{ needs.detect-changes.outputs.transport-version }}" # Push Mintlify changelog push-mintlify-changelog: needs: [ check-skip, detect-changes, test-core, test-core-integrations, test-core-unit, approve-flaky-test-core, test-proxy-e2e, resolve-cli-versions, test-cli-harness, test-framework, test-plugins, test-bifrost-http, test-live, test-migrations, test-agentgateway, test-api-integrations, test-cost-accuracy, test-docker-image-amd64, test-docker-image-arm64, core-release, framework-release, plugins-release, bifrost-http-release, ] if: "always() && needs.check-skip.outputs.should-skip != 'true' && needs.detect-changes.result == 'success' && (needs.test-core.result == 'success' || needs.test-core.result == 'skipped' || (needs.test-core.result == 'failure' && needs.approve-flaky-test-core.result == 'success')) && (needs.test-proxy-e2e.result == 'success' || needs.test-proxy-e2e.result == 'skipped') && (needs.test-core-integrations.result == 'success' || needs.test-core-integrations.result == 'skipped') && (needs.test-core-unit.result == 'success' || needs.test-core-unit.result == 'skipped') && (needs.test-cli-harness.result == 'success' || needs.test-cli-harness.result == 'skipped') && (needs.resolve-cli-versions.result == 'success' || needs.resolve-cli-versions.result == 'skipped') && (needs.test-framework.result == 'success' || needs.test-framework.result == 'skipped') && (needs.test-plugins.result == 'success' || needs.test-plugins.result == 'skipped') && (needs.test-bifrost-http.result == 'success' || needs.test-bifrost-http.result == 'skipped') && (needs.test-live.result == 'success' || needs.test-live.result == 'skipped') && (needs.test-migrations.result == 'success' || needs.test-migrations.result == 'skipped') && (needs.test-agentgateway.result == 'success' || needs.test-agentgateway.result == 'skipped') && (needs.test-api-integrations.result == 'success' || needs.test-api-integrations.result == 'skipped') && (needs.test-cost-accuracy.result == 'success' || needs.test-cost-accuracy.result == 'skipped') && (needs.test-docker-image-amd64.result == 'success' || needs.test-docker-image-amd64.result == 'skipped') && (needs.test-docker-image-arm64.result == 'success' || needs.test-docker-image-arm64.result == 'skipped') && (needs.detect-changes.outputs.core-needs-release == 'false' || needs.core-release.result == 'success' || needs.core-release.result == 'skipped') && (needs.detect-changes.outputs.framework-needs-release == 'false' || needs.framework-release.result == 'success' || needs.framework-release.result == 'skipped') && (needs.detect-changes.outputs.plugins-need-release == 'false' || needs.plugins-release.result == 'success' || needs.plugins-release.result == 'skipped') && (needs.detect-changes.outputs.bifrost-http-needs-release == 'false' || needs.bifrost-http-release.result == 'success' || needs.bifrost-http-release.result == 'skipped')" runs-on: ubuntu-latest permissions: contents: write steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 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 token: ${{ secrets.GH_TOKEN }} - name: Push Mintlify changelog env: # Public origin serving the R2 bucket that test-core and # test-cli-harness publish their reports to. Unset means the changelog # simply omits the test-reports table - see the script for why it is # not hardcoded. TEST_REPORTS_BASE_URL: ${{ vars.TEST_REPORTS_BASE_URL }} # The CLI harness legs that actually ran. Normally latest/latest-1/ # latest-2, but resolve-cli-versions degrades to a single "pinned" leg # when npm resolution fails - and the report objects are named after # whichever labels it emitted, so the changelog table has to follow # them rather than assume the three. # Guarded: this job's `if:` accepts resolve-cli-versions being skipped, # and a skipped job's output is the empty string - fromJSON('') is a # workflow-level error that fails the step before the script runs. An # empty value here is meaningful rather than missing: it tells the # script no CLI legs ran, so it emits no rows instead of defaulting to # the three standard labels and linking reports that do not exist. CLI_HARNESS_LABELS: ${{ needs.resolve-cli-versions.outputs.matrix && join(fromJSON(needs.resolve-cli-versions.outputs.matrix).*.label, ' ') || '' }} run: | ./.github/workflows/scripts/push-mintlify-changelog.sh "${{ needs.detect-changes.outputs.transport-version }}" # Notification notify: needs: [ check-skip, approve-run, detect-changes, test-core, test-proxy-e2e, resolve-cli-versions, test-cli-harness, test-framework, test-plugins, test-bifrost-http, test-live, test-migrations, test-agentgateway, test-cost-accuracy, test-docker-image-amd64, test-docker-image-arm64, core-release, framework-release, plugins-release, bifrost-http-release, docker-manifest, docker-manifest-ubi9, ] # A run rejected at the phone gate ran nothing, so there is no release to # report; without this clause Discord would get an all-skipped "complete". if: "always() && needs.check-skip.outputs.should-skip != 'true' && needs.approve-run.result == 'success'" runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: block allowed-endpoints: > _http._tcp.azure.archive.ubuntu.com:443 _https._tcp.dl.google.com:443 motd.ubuntu.com:443 _https._tcp.esm.ubuntu.com:443 _https._tcp.motd.ubuntu.com:443 _https._tcp.packages.microsoft.com:443 azure.archive.ubuntu.com:80 discord.com:443 discordapp.com:443 dl.google.com:443 esm.ubuntu.com:443 packages.microsoft.com:443 - name: Install jq run: | sudo apt-get update sudo apt-get install -y jq - name: Discord Notification env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} run: | # Build status summary CORE_STATUS="ā­ļø Skipped" FRAMEWORK_STATUS="ā­ļø Skipped" PLUGINS_STATUS="ā­ļø Skipped" BIFROST_STATUS="ā­ļø Skipped" if [ "${{ needs.core-release.result }}" = "success" ]; then CORE_STATUS="āœ… Released v${{ needs.detect-changes.outputs.core-version }}" elif [ "${{ needs.core-release.result }}" = "failure" ]; then CORE_STATUS="āŒ Failed" fi if [ "${{ needs.framework-release.result }}" = "success" ]; then FRAMEWORK_STATUS="āœ… Released v${{ needs.detect-changes.outputs.framework-version }}" elif [ "${{ needs.framework-release.result }}" = "failure" ]; then FRAMEWORK_STATUS="āŒ Failed" fi if [ "${{ needs.plugins-release.result }}" = "success" ]; then PLUGINS_STATUS="āœ… Released plugins" elif [ "${{ needs.plugins-release.result }}" = "failure" ]; then PLUGINS_STATUS="āŒ Failed" fi if [ "${{ needs.bifrost-http-release.result }}" = "success" ]; then BIFROST_STATUS="āœ… Released v${{ needs.detect-changes.outputs.transport-version }}" elif [ "${{ needs.bifrost-http-release.result }}" = "failure" ]; then BIFROST_STATUS="āŒ Failed" fi # Build the message with proper formatting MESSAGE=$(printf "šŸš€ **Release Pipeline Complete**\n\n**Components:**\n• Core: %s\n• Framework: %s\n• Plugins: %s\n• Bifrost HTTP: %s\n\n**Details:**\n• Branch: \`${{ github.ref_name }}\`\n• Commit: \`%.8s\`\n• Author: %s\n\n[View Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" "$CORE_STATUS" "$FRAMEWORK_STATUS" "$PLUGINS_STATUS" "$BIFROST_STATUS" "${{ github.sha }}" "${{ github.actor }}") payload="$(jq -n --arg content "$MESSAGE" '{content:$content}')" curl -sS -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK"