chore: import upstream snapshot with attribution
TypeScript SDK Compatibility V1.x E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / TypeScript SDK Compatibility V1.x E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
TypeScript SDK E2E Tests / TypeScript SDK E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
Python SDK E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK E2E Tests / Python SDK E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK E2E Tests / build-opik (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Python SDK Compatibility V1.x E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK E2E Tests / build-opik (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer E2E Tests Python ${{matrix.python_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer Integration Smoke Tests (push) Has been cancelled
🐙 Code Quality / detect (push) Has been cancelled
🐙 Code Quality / lint (${{ matrix.leg.name }}) (push) Has been cancelled
🐙 Code Quality / summary (push) Has been cancelled
TypeScript SDK Library Integration Tests / Check Secrets (push) Has been cancelled
TypeScript SDK Library Integration Tests / opik-vercel (Vercel AI SDK / eve) (push) Has been cancelled
SDK Library Integration Tests Runner / Check Secrets (push) Has been cancelled
SDK Library Integration Tests Runner / Missed OpenAI API Key Warning (push) Has been cancelled
SDK Library Integration Tests Runner / Build (push) Has been cancelled
SDK Library Integration Tests Runner / openai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_legacy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / llama_index_tests (push) Has been cancelled
SDK Library Integration Tests Runner / anthropic_tests (push) Has been cancelled
SDK Library Integration Tests Runner / mistral_tests (push) Has been cancelled
SDK Library Integration Tests Runner / groq_tests (push) Has been cancelled
SDK Library Integration Tests Runner / aisuite_tests (push) Has been cancelled
SDK Library Integration Tests Runner / haystack_tests (push) Has been cancelled
SDK Library Integration Tests Runner / dspy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v1_tests (push) Has been cancelled
SDK Library Integration Tests Runner / genai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_legacy_1_3_0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / evaluation_metrics_tests (push) Has been cancelled
SDK Library Integration Tests Runner / bedrock_tests (push) Has been cancelled
SDK Library Integration Tests Runner / litellm_tests (push) Has been cancelled
SDK Library Integration Tests Runner / harbor_tests (push) Has been cancelled
SDK Library Integration Tests Runner / Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / render-equality (push) Has been cancelled
Opik Optimizer - Unit Tests / Opik Optimizer Unit Tests Python ${{matrix.python_version}} (push) Has been cancelled
Python BE E2E Tests / Python BE E2E (push) Has been cancelled
Python Backend Tests / run-python-backend-tests (push) Has been cancelled
Python SDK Unit Tests / Python SDK Unit Tests ${{matrix.python_version}} (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
SDK E2E Libraries Integration Tests / Check Secrets (push) Has been cancelled
SDK E2E Libraries Integration Tests / Missed OpenAI API Key Warning (push) Has been cancelled
SDK E2E Libraries Integration Tests / build-opik (push) Has been cancelled
SDK E2E Libraries Integration Tests / E2E Lib Integration Python ${{matrix.python_version}} (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-gemini) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-langchain) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-openai) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-otel) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-vercel) (push) Has been cancelled
TypeScript SDK Build & Publish / build-and-publish (push) Has been cancelled
TypeScript SDK Unit Tests / Test on Node ${{ matrix.node-version }} (push) Has been cancelled
Backend Tests / discover-tests (push) Has been cancelled
Backend Tests / ${{ matrix.name }} (push) Has been cancelled
Build and Publish SDK / build-and-publish (push) Has been cancelled
Build Opik Docker Images / set-version (push) Has been cancelled
Build Opik Docker Images / build-backend (push) Has been cancelled
Build Opik Docker Images / build-sandbox-executor-python (push) Has been cancelled
Build Opik Docker Images / build-python-backend (push) Has been cancelled
Build Opik Docker Images / build-frontend (push) Has been cancelled
Build Opik Docker Images / create-git-tag (push) Has been cancelled
ClickHouse Migration Cluster Check / validate-clickhouse-migrations (push) Has been cancelled
Docs - Publish / run (push) Has been cancelled
E2E Tests - Post Merge (v2) / 🧪 E2E v2 Tests (${{ github.event.inputs.tier || 't1' }}) (push) Has been cancelled
E2E Tests - Post Merge (v2) / 📢 Slack Notification (push) Has been cancelled
Frontend Unit Tests / Test on Node 20 (push) Has been cancelled
Guardrails E2E Tests / Select Python version matrix (push) Has been cancelled
Guardrails E2E Tests / Guardrails E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Guardrails E2E Tests / 📢 Slack Notification (push) Has been cancelled
Guardrails Backend Unit Tests / Guardrails Backend Unit Tests (push) Has been cancelled
Guardrails Backend Unit Tests / 📢 Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v3.21.0) (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v4.2.0) (push) Has been cancelled
Lint Opik Helm Chart / unittest-helm-chart (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:44 +08:00
commit 5a558eb09e
11579 changed files with 1795921 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
# This is a comment.
# Each line is a file pattern followed by one or more owners.
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
# @comet-ml/comet-opik-devs will be requested for
# review when someone opens a pull request.
* @comet-ml/comet-opik-devs # This is an inline comment.
*.md @comet-ml/product @comet-ml/comet-opik-devs
*.mdx @comet-ml/product @comet-ml/comet-opik-devs
/.github/ISSUE_TEMPLATE/ @comet-ml/product @comet-ml/comet-opik-devs
/.github/actions/ @comet-ml/comet-devops @comet-ml/comet-qa @comet-ml/comet-opik-devs
/.github/workflows/ @comet-ml/comet-devops @comet-ml/comet-qa @comet-ml/comet-opik-devs
/apps/opik-documentation/ @comet-ml/product @comet-ml/comet-opik-devs
/deployment/ @comet-ml/comet-devops @comet-ml/deployment-team @comet-ml/comet-opik-devs
/sdks/opik_optimizer @comet-ml/comet-opik-optimizer-devs @comet-ml/comet-opik-devs
/tests_end_to_end/ @comet-ml/comet-qa @comet-ml/comet-opik-devs
+56
View File
@@ -0,0 +1,56 @@
name: Bug Report
description: File a bug report.
title: "[Bug]: "
labels: ["bug", "triage"]
body:
- type: markdown
attributes:
value: |
Thank you for submitting a bug report.
#### To help us resolve your issue, please provide fill in this bug report template.
- type: checkboxes
attributes:
label: What component(s) are affected?
options:
- label: Opik Python SDK
required: false
- label: Opik Typescript SDK
required: false
- label: Opik Agent Optimizer SDK
required: false
- label: Opik UI
required: false
- label: Opik Server
required: false
- label: Documentation
required: false
validations:
required: false
- type: textarea
validations:
required: true
attributes:
label: Opik version
placeholder: The Opik version, you can find the Opik version by running `opik.__version__` in Python.
value: |
- Opik version: x.x.x
- type: textarea
attributes:
label: Describe the problem
description: |
Describe the problem clearly here, you should include both the expected behavior and the actual behavior.
validations:
required: true
- type: textarea
attributes:
label: Reproduction steps and code snippets
description: Describe how to reproduce your bug. Please provide detailed steps and include code snippets if possible.
- type: textarea
attributes:
label: Error logs or stack trace
description: Please provide the error messages, logs, or stack trace that appear when your bug is reproduced, if possible.
- type: textarea
attributes:
label: Healthcheck results
description: Run `opik healthcheck` to perform some basic checks which might be useful for troubleshooting your issue. Paste your results as a text or a screenshot.
placeholder: CLI output from 'opik healthcheck'...
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Explore Opik Documentation
url: https://www.comet.com/docs/opik
about: New to Opik? read our documentation to get setup.
- name: Ask Opik Community Support
url: https://chat.comet.com
about: Ask usage questions in the community before opening an issue.
@@ -0,0 +1,27 @@
name: Feature Request
description: File a feature request.
title: "[FR]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thank you for submitting a feature request.
#### To help us prioritize your request, please provide fill in this feature request template.
- type: textarea
attributes:
label: Proposal summary
description: Provide a brief summary of your feature request. Adding a code (or pseudo-code) snippet that describes your use-case or the way you would like the feature to work is highly appreciated. The details help the team to process your request faster.
validations:
required: true
- type: textarea
validations:
required: false
attributes:
label: Motivation
description: |
Describe the motivation for your feature request, this can include:
- What problem are you trying to solve?
- How are you currently solving this problem?
- What are the benefits of this feature?
+5
View File
@@ -0,0 +1,5 @@
self-hosted-runner:
labels:
# Org-hosted larger runner. Not a public GitHub label, so actionlint
# needs it declared here to avoid a false "unknown label" error.
- ubuntu-latest-m
@@ -0,0 +1,39 @@
---
name: Install OPIK SDK and run E2E lib tests
description: Install OPIK SDK with test dependencies and executes E2E integration tests for particular library
inputs:
library_name:
required: true
description: "The library name"
python_version:
required: true
description: "Python version"
runs:
using: composite
steps:
- name: Install OPIK, test requirements, and run tests for ${{ inputs.library_name }}
shell: bash
run: |
# create virtual environment
uv venv ${{ inputs.library_name }}-env --python ${{ inputs.python_version }}
source ${{ inputs.library_name }}-env/bin/activate
cd ${{ github.workspace }}/sdks/python
echo "Installing OPIK"
uv pip install .
echo "Installing test dependencies"
cd ./tests
uv pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
uv pip install --no-cache-dir --disable-pip-version-check -r e2e_library_integration/${{ inputs.library_name }}/requirements.txt
uv pip list
echo "Running tests"
export OPIK_CONSOLE_LOGGING_LEVEL=DEBUG
cd ./e2e_library_integration/${{ inputs.library_name }}/
python -m pytest -vv .
# Deactivate virtual environment
deactivate
@@ -0,0 +1,79 @@
---
name: NPM Token Preflight
description: |
Validate NPM_TOKEN before publish: confirms the token authenticates against
the registry and is authorized to publish the named package. Fails fast with
an actionable message so we never reach `npm publish` with a bad token.
inputs:
npm_token:
required: true
description: NPM_TOKEN secret value (passed via env, never echoed)
package_name:
required: true
description: Package name to verify publish access for (e.g. opik, opik-openai)
registry_url:
required: false
description: NPM registry URL
default: "https://registry.npmjs.org"
runs:
using: composite
steps:
- name: NPM token preflight (${{ inputs.package_name }})
shell: bash
env:
NODE_AUTH_TOKEN: ${{ inputs.npm_token }}
PACKAGE_NAME: ${{ inputs.package_name }}
REGISTRY_URL: ${{ inputs.registry_url }}
run: |
set -e
if [ -z "${NODE_AUTH_TOKEN:-}" ]; then
echo "::error title=NPM preflight::NPM_TOKEN is empty. The secret is not configured or not exposed to this job."
exit 1
fi
REGISTRY_HOST="${REGISTRY_URL#https://}"
REGISTRY_HOST="${REGISTRY_HOST#http://}"
REGISTRY_HOST="${REGISTRY_HOST%/}"
TMP_NPMRC="$(mktemp)"
trap 'rm -f "$TMP_NPMRC"' EXIT
# shellcheck disable=SC2016
# Intentional literal ${NODE_AUTH_TOKEN}: npm expands it at .npmrc read time,
# keeping the token out of process args and shell history.
printf '//%s/:_authToken=${NODE_AUTH_TOKEN}\nregistry=%s\n' "$REGISTRY_HOST" "$REGISTRY_URL" > "$TMP_NPMRC"
echo "==> npm whoami"
if ! WHOAMI_OUT=$(npm --userconfig "$TMP_NPMRC" whoami 2>&1); then
echo "::error title=NPM preflight::npm whoami failed. NPM_TOKEN is invalid, expired, or revoked."
echo "Output:"
echo "$WHOAMI_OUT"
exit 1
fi
echo "Authenticated as: $WHOAMI_OUT"
echo "==> npm access list packages (filtering for ${PACKAGE_NAME})"
if ! ACCESS_OUT=$(npm --userconfig "$TMP_NPMRC" access list packages 2>&1); then
echo "::warning title=NPM preflight::npm access list packages failed; skipping authorization check."
echo "Output:"
echo "$ACCESS_OUT"
echo "Proceeding — publish step will be the source of truth."
exit 0
fi
# Escape ERE metacharacters in PACKAGE_NAME so e.g. a future name containing
# `.` doesn't match a different package with any char at that position.
PACKAGE_NAME_ERE=$(printf '%s' "$PACKAGE_NAME" | sed -e 's/[][\.*+?(){}|^$\\]/\\&/g')
if echo "$ACCESS_OUT" | grep -qE "^[[:space:]]*\"?${PACKAGE_NAME_ERE}\"?[[:space:]:]+\"?(read-write|write)\"?"; then
echo "Token has write access to ${PACKAGE_NAME}."
exit 0
fi
# Classic (legacy) tokens don't always return per-package scopes via this command.
# If the package is missing from the list but whoami succeeded, treat as pass-with-warning
# rather than block — publish will still surface a real auth error if it exists.
echo "::warning title=NPM preflight::Could not confirm write access to ${PACKAGE_NAME} from \`npm access list packages\` output."
echo "This is expected for classic automation tokens; granular tokens should list the package explicitly."
echo "Continuing — publish step will surface any real authorization error."
@@ -0,0 +1,81 @@
---
name: PyPI Token Preflight
description: |
Validate PYPI_API_TOKEN before publish: confirms the token has the expected
format and is accepted by the PyPI upload endpoint. Fails fast with an
actionable message so we never reach `twine upload` with a bad token.
inputs:
pypi_token:
required: true
description: PYPI_API_TOKEN secret value (passed via env, never echoed)
package_name:
required: true
description: Package name being published (for diagnostics only; PyPI does not gate at this layer)
upload_url:
required: false
description: PyPI upload endpoint
default: "https://upload.pypi.org/legacy/"
runs:
using: composite
steps:
- name: PyPI token preflight (${{ inputs.package_name }})
shell: bash
env:
PYPI_TOKEN: ${{ inputs.pypi_token }}
PACKAGE_NAME: ${{ inputs.package_name }}
UPLOAD_URL: ${{ inputs.upload_url }}
run: |
set -e
if [ -z "${PYPI_TOKEN:-}" ]; then
echo "::error title=PyPI preflight::PYPI_API_TOKEN is empty. The secret is not configured or not exposed to this job."
exit 1
fi
if [[ "$PYPI_TOKEN" != pypi-* ]]; then
echo "::error title=PyPI preflight::PYPI_API_TOKEN does not start with 'pypi-'. PyPI API tokens must use the 'pypi-' prefix."
exit 1
fi
echo "Token format OK (pypi-* prefix)."
echo "==> Probing PyPI upload endpoint: ${UPLOAD_URL}"
# POST with no multipart body. PyPI's response distinguishes auth from payload errors:
# 401/403 -> token rejected (bad/expired/wrong-scope)
# 400/422 -> token accepted, but request body invalid (this is the success case for a preflight)
# 200 -> shouldn't happen without a real upload, but also indicates auth OK
HTTP_CODE=$(curl -s -o /tmp/pypi_probe_body -w "%{http_code}" \
-X POST \
-u "__token__:${PYPI_TOKEN}" \
-H "User-Agent: opik-release-preflight" \
"${UPLOAD_URL}" || echo "000")
echo "HTTP ${HTTP_CODE}"
case "$HTTP_CODE" in
400|422)
echo "Token accepted by PyPI (HTTP ${HTTP_CODE} = empty/invalid payload, but auth passed)."
exit 0
;;
200)
echo "::warning title=PyPI preflight::Unexpected 200 OK from an empty POST; treating as auth-success."
exit 0
;;
401|403)
echo "::error title=PyPI preflight::PYPI_API_TOKEN was rejected by ${UPLOAD_URL} (HTTP ${HTTP_CODE}). Token is invalid, expired, revoked, or not scoped for ${PACKAGE_NAME}."
echo "Response body:"
cat /tmp/pypi_probe_body || true
exit 1
;;
000)
echo "::error title=PyPI preflight::Could not reach ${UPLOAD_URL} at all. Network or DNS issue on the runner."
exit 1
;;
*)
echo "::warning title=PyPI preflight::Unexpected HTTP ${HTTP_CODE} from ${UPLOAD_URL}. Continuing — publish step will be the source of truth."
echo "Response body:"
cat /tmp/pypi_probe_body || true
exit 0
;;
esac
@@ -0,0 +1,59 @@
name: Select E2E version matrix
description: >
Emits the full version array or a reduced [min, max] array depending on whether
the event's changed files match the given SDK-touching regex. Full matrix on
workflow_dispatch and when any changed file matches the regex; min+max otherwise.
inputs:
all-versions:
required: true
description: JSON array of all supported versions (e.g. '["3.10","3.14"]').
match-regex:
required: true
description: POSIX extended regex; any changed file matching it forces the full matrix.
github-token:
required: true
description: Token used to list PR/commit files via the GitHub API.
outputs:
versions:
description: JSON array of versions the caller should feed into its strategy matrix.
value: ${{ steps.pick.outputs.versions }}
runs:
using: composite
steps:
- id: pick
shell: bash
env:
GH_TOKEN: ${{ inputs.github-token }}
ALL: ${{ inputs.all-versions }}
REGEX: ${{ inputs.match-regex }}
REPO: ${{ github.repository }}
EVENT: ${{ github.event_name }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BEFORE: ${{ github.event.before }}
SHA: ${{ github.sha }}
run: |
set -euo pipefail
if [ "$EVENT" = "workflow_dispatch" ]; then
echo "versions=$ALL" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$EVENT" = "pull_request" ]; then
FILES=$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/files" --jq '.[].filename')
elif [ -n "$BEFORE" ] && [ "$BEFORE" != "0000000000000000000000000000000000000000" ]; then
# Aggregate across the full push range so multi-commit pushes (rebase-merge, direct pushes)
# don't miss SDK-touching files landed in non-tip commits.
FILES=$(gh api "repos/$REPO/compare/$BEFORE...$SHA" --jq '.files[].filename')
else
# Branch creation / force-push: before is zero or missing; fall back to tip.
FILES=$(gh api "repos/$REPO/commits/$SHA" --jq '.files[].filename')
fi
# Default to full matrix on empty file list so a degenerate API response
# never silently under-tests.
if [ -z "$FILES" ] || echo "$FILES" | grep -Eq "$REGEX"; then
echo "versions=$ALL" >> "$GITHUB_OUTPUT"
else
echo "versions=$(echo "$ALL" | jq -c '[.[0], .[-1]]')" >> "$GITHUB_OUTPUT"
fi
@@ -0,0 +1,279 @@
---
name: TypeScript SDK Build & Publish
description: Common steps for building and publishing TypeScript SDK packages
inputs:
working_directory:
required: true
description: Working directory for the package (e.g., sdks/typescript or sdks/typescript/src/opik/integrations/opik-openai)
version:
required: false
description: Version to use (if not provided, will generate from package.json + SHA)
is_release:
required: false
description: Whether this is a release (publishes to NPM)
default: "false"
package_name:
required: false
description: Package name for commit message and summary (defaults to package.json name)
npm_access:
required: false
description: NPM publish access (defaults to empty, use 'public' for scoped packages)
default: ""
enable_cache:
required: false
description: Enable npm cache
default: "false"
cache_dependency_path:
required: false
description: Path to package-lock.json for cache
default: ""
test_package_installation:
required: false
description: Run npm pack --dry-run test
default: "false"
npm_token:
required: false
description: NPM token for publishing
github_token:
required: false
description: GitHub token for pushing changes
skip_commit_push:
required: false
description: Skip commit and push (for parallel builds in release)
default: "false"
outputs:
version:
description: Generated or provided version
value: ${{ steps.set_outputs.outputs.version }}
package_name:
description: Package name from package.json
value: ${{ steps.set_outputs.outputs.package_name }}
runs:
using: composite
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"
cache: ${{ inputs.enable_cache == 'true' && 'npm' || '' }}
cache-dependency-path: ${{ inputs.cache_dependency_path }}
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.working_directory }}
run: npm ci
- name: Generate version
id: version
shell: bash
working-directory: ${{ inputs.working_directory }}
env:
# Read inputs via env, not `${{ }}` substitution, so a malicious dispatch
# input can't inject shell tokens (OWASP / GHA security hardening).
INPUT_VERSION: ${{ inputs.version }}
run: |
if [ -n "${INPUT_VERSION}" ]; then
# Validate against a permissive semver regex (X.Y.Z + optional
# prerelease/build suffix). Anything else fails fast.
if ! [[ "${INPUT_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?(\+[A-Za-z0-9.-]+)?$ ]]; then
echo "::error title=Generate version::inputs.version='${INPUT_VERSION}' does not match the expected semver shape (X.Y.Z[-pre][+build])."
exit 1
fi
VERSION="${INPUT_VERSION}"
else
# For push/PR: use package.json version with short SHA (not published).
PACKAGE_VERSION=$(node -p "require('./package.json').version")
SHORT_SHA=$(git rev-parse --short HEAD)
VERSION="${PACKAGE_VERSION}-${SHORT_SHA}"
fi
echo "VERSION=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Generated version: ${VERSION}"
- name: Update version
if: ${{ inputs.is_release == 'true' }}
shell: bash
working-directory: ${{ inputs.working_directory }}
env:
# `RELEASE_VERSION` comes via env (not `${{ }}` substitution) so it can't
# inject shell tokens into the `npm version` command (OWASP). Even though
# the `Generate version` step now regex-validates inputs.version, env+
# quoted is the defense-in-depth shape recommended by GHA security docs.
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
# `--allow-same-version` treats "package.json already at this version" as a
# successful no-op instead of erroring `Version not changed`. The operator's
# intent on dispatch is "publish this version" — package.json already being
# at the target value is bookkeeping that npm publish doesn't care about, so
# the bump step shouldn't either. Without this, replay/re-dispatch of an
# already-released version (where the prior run's commit-version step landed
# on the dispatched ref) fails here before reaching the publish step's own
# already-published skip.
run: npm version "${RELEASE_VERSION}" --no-git-tag-version --allow-same-version
- name: Build package
shell: bash
working-directory: ${{ inputs.working_directory }}
run: npm run build
- name: Run tests
shell: bash
working-directory: ${{ inputs.working_directory }}
run: npm test
- name: Run linting
shell: bash
working-directory: ${{ inputs.working_directory }}
run: npm run lint
- name: Run type checking
shell: bash
working-directory: ${{ inputs.working_directory }}
run: npm run typecheck
- name: Test package installation (dry-run)
if: ${{ inputs.test_package_installation == 'true' }}
shell: bash
working-directory: ${{ inputs.working_directory }}
run: |
echo "Testing package installation..."
npm pack --dry-run
echo "Package can be installed successfully"
- name: Resolve npm package name from package.json
id: npm_name
if: ${{ inputs.is_release == 'true' }}
shell: bash
working-directory: ${{ inputs.working_directory }}
run: |
NPM_PACKAGE_NAME=$(node -p "require('./package.json').name")
echo "name=${NPM_PACKAGE_NAME}" >> "$GITHUB_OUTPUT"
echo "Resolved npm package name: ${NPM_PACKAGE_NAME}"
- name: NPM token preflight
if: ${{ inputs.is_release == 'true' }}
uses: ./.github/actions/npm-token-preflight
with:
npm_token: ${{ inputs.npm_token }}
package_name: ${{ steps.npm_name.outputs.name }}
- name: Publish to NPM (with retry)
id: npm_publish
if: ${{ inputs.is_release == 'true' }}
shell: bash
working-directory: ${{ inputs.working_directory }}
run: |
NEW_VERSION="${{ steps.version.outputs.VERSION }}"
PACKAGE_NAME=$(node -p "require('./package.json').name")
echo "Publishing to NPM: $PACKAGE_NAME@$NEW_VERSION"
# Retry logic: 3 attempts with 10 second delays and 3-minute timeout per attempt
MAX_ATTEMPTS=3
ATTEMPT=1
SUCCESS=false
TIMEOUT_SECONDS=180 # 3 minutes per attempt
PUBLISH_CMD="npm publish"
if [ -n "${{ inputs.npm_access }}" ]; then
PUBLISH_CMD="npm publish --access ${{ inputs.npm_access }}"
fi
ALREADY_PUBLISHED=false
PUBLISH_LOG=$(mktemp)
trap 'rm -f "$PUBLISH_LOG"' EXIT
while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
echo "Publish attempt $ATTEMPT of $MAX_ATTEMPTS (timeout: ${TIMEOUT_SECONDS}s)..."
# Run the publish via `if pipeline; then ... else ...; fi`. This shape is
# load-bearing for three reasons; do not rewrite to a bare pipeline:
# 1. The `if` condition shields the pipeline from `set -e` (GitHub Actions
# `shell: bash` uses `bash --noprofile --norc -eo pipefail`; without
# this shield, a non-zero pipeline kills the step before we evaluate
# the 403-skip below).
# 2. PIPESTATUS is preserved inside the `else` branch (a trailing
# `|| true` would clobber PIPESTATUS to reflect only `true`, masking
# every failure as exit-0 and silently turning real publish failures
# into false-green CI).
# 3. `tee` streams output live AND captures it to PUBLISH_LOG for the
# grep-based 403-already-published check.
if timeout $TIMEOUT_SECONDS $PUBLISH_CMD 2>&1 | tee "$PUBLISH_LOG"; then
SUCCESS=true
break
else
EXIT_CODE=${PIPESTATUS[0]}
if [ "$EXIT_CODE" -eq 124 ]; then
echo "Attempt $ATTEMPT timed out after ${TIMEOUT_SECONDS}s"
else
echo "Attempt $ATTEMPT failed with exit code $EXIT_CODE"
fi
# Non-retryable: version already published. The desired end state is
# achieved; treat as a skip-with-warning so concurrent / replay
# dispatches don't fail CI. Match BOTH `E403` and the specific phrase
# `cannot publish over` so we don't accidentally treat other 403s
# (e.g. `You may not perform that action with these credentials`, which
# means the token lacks publish scope on this package) as already-
# published — those must still fail loudly.
if grep -q "E403" "$PUBLISH_LOG" && grep -qi "cannot publish over" "$PUBLISH_LOG"; then
ALREADY_PUBLISHED=true
echo "::warning title=NPM publish skipped::${PACKAGE_NAME}@${NEW_VERSION} is already published; treating as success."
break
fi
if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then
echo "Retrying in 10 seconds..."
sleep 10
fi
ATTEMPT=$((ATTEMPT + 1))
fi
done
if [ "$SUCCESS" = false ] && [ "$ALREADY_PUBLISHED" = false ]; then
echo "Failed to publish after $MAX_ATTEMPTS attempts"
exit 1
fi
# Save package name and version for summary
echo "PACKAGE_NAME=$PACKAGE_NAME" >> $GITHUB_ENV
echo "PUBLISHED_VERSION=$NEW_VERSION" >> $GITHUB_ENV
env:
NODE_AUTH_TOKEN: ${{ inputs.npm_token }}
- name: Commit version changes
if: ${{ inputs.is_release == 'true' && inputs.skip_commit_push != 'true' }}
shell: bash
working-directory: ${{ inputs.working_directory }}
run: |
git config --local user.email "github-actions@comet.com"
git config --local user.name "github-actions"
git add package.json package-lock.json
PACKAGE_NAME="${{ inputs.package_name }}"
if [ -z "$PACKAGE_NAME" ]; then
PACKAGE_NAME=$(node -p "require('./package.json').name")
fi
git commit -m "Update $PACKAGE_NAME version to ${{ steps.version.outputs.VERSION }}"
- name: Push changes
if: ${{ inputs.is_release == 'true' && inputs.skip_commit_push != 'true' }}
uses: ad-m/github-push-action@v0.8.0
with:
github_token: ${{ inputs.github_token }}
branch: ${{ github.ref }}
force_with_lease: true
- name: Set outputs
id: set_outputs
shell: bash
working-directory: ${{ inputs.working_directory }}
run: |
echo "version=${{ steps.version.outputs.VERSION }}" >> $GITHUB_OUTPUT
# Get package name from package.json
PACKAGE_NAME=$(node -p "require('./package.json').name")
echo "package_name=$PACKAGE_NAME" >> $GITHUB_OUTPUT
+553
View File
@@ -0,0 +1,553 @@
# Copilot Code Review Instructions
> **Scope:** These guidelines apply to all Opik applications including backend, frontend, and SDKs. Use the appropriate sections based on the code being reviewed.
When Copilot automatically reviews pull requests, use the following guidelines to structure feedback and ensure consistency across the entire Opik project.
---
## Project Overview
Opik is a comprehensive observability and AI evaluation platform with multiple applications:
- **Backend**: Java-based REST API with MySQL and ClickHouse databases
- **Frontend**: React/TypeScript application with modern UI components
- **Python SDK**: Client library for Python applications
- **TypeScript SDK**: Client library for TypeScript/JavaScript applications
- **Documentation**: Comprehensive documentation site
- **Testing**: End-to-end and load testing suites
## 1. Git Workflow & Branch Management
### Branch Naming Convention
```
{username}/{ticket}-{summary}
```
**Examples:**
```
andrescrz/OPIK-2236-add-documentation-and-user-facing-distinction-to-pr-template
someuser/issue-1234-some-task
someotheruser/NA-some-other-task
```
### Commit Message Standards
Use component types to categorize changes (optional but recommended):
- `[DOCS]` - Documentation updates, README changes, comments, swagger/OpenAPI documentation
- `[FE]` - Frontend changes (React, TypeScript, UI components)
- `[BE]` - Backend changes (Java, API endpoints, services)
- `[SDK]` - SDK changes (Python, TypeScript SDKs)
**Examples:**
```bash
# ✅ Recommended format
git commit -m "[OPIK-1234] [FE] Add project custom metrics UI dashboard"
git commit -m "[OPIK-1234] [BE] Add create trace endpoint"
# ✅ Also acceptable
git commit -m "[OPIK-1234] Add project custom metrics UI dashboard"
```
### Pull Request Guidelines
**Title Format:** `[{ticket}] [{component}] {summary}`
**Required Sections:**
- **Details**: What the change does, why it was made, and any design decisions
- **Change checklist**: User facing and Documentation update checkboxes
- **Issues**: GitHub issue or Jira ticket references
- **Testing**: Scenarios covered by tests and steps to reproduce
- **Documentation**: List of docs updated or new configuration introduced
---
## 2. Backend (Java) Review Guidelines
### Technology Stack
- **Language**: Java 25
- **Framework**: Dropwizard 5.0.0
- **Database**: MySQL 9.7.0, ClickHouse 0.9.0
- **Build Tool**: Maven with Spotless 3.5.1
- **Testing**: JUnit 5, Testcontainers, WireMock
### Architecture Requirements
- **Layered Architecture**: Resources → Services → DAOs → Models
- **Separation of Concerns**: Each layer has a single responsibility
- **Dependency Injection**: Use Guice with `@Singleton` and `@RequiredArgsConstructor`
- **Reactive Design**: Applications must be reactive, non-blocking, and horizontally scalable
### API Design Standards
- **REST Endpoints**: Follow standard HTTP methods and URL patterns
- **Validation**: Use `@Valid` and Jakarta validation annotations
- **Documentation**: Include `@Operation` with proper `operationId`
- **Response Codes**: Use appropriate HTTP status codes (200, 201, 400, 404, 500)
**Example Controller Pattern:**
```java
@Path("/api/v1/resources")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@RequiredArgsConstructor(onConstructor_ = @Inject)
public class ResourcesResource {
private final @NonNull ResourceService resourceService;
@POST
@Operation(summary = "Create resource", operationId = "createResource")
public Response createResource(@Valid ResourceCreateRequest request) {
var resource = resourceService.createResource(request);
return Response.status(Response.Status.CREATED)
.entity(resource)
.build();
}
}
```
### Database Access Patterns
- **Always use transactions** for MySQL reads/writes
- **Use TransactionTemplate** with READ_ONLY or WRITE types
- **JDBI3 interfaces** for DAO implementations
- **IdGenerator** for UUID v7 generation
**Example Service Pattern:**
```java
@Singleton
@RequiredArgsConstructor(onConstructor_ = @Inject)
public class ResourceService {
private final @NonNull ResourceDao resourceDao;
private final @NonNull IdGenerator idGenerator;
private final @NonNull TransactionTemplate transactionTemplate;
public ResourceResponse createResource(ResourceCreateRequest request) {
return transactionTemplate.inTransaction(WRITE, handle -> {
var repository = handle.attach(ResourceDao.class);
var resource = Resource.builder()
.id(idGenerator.generate())
.name(request.getName())
.createdAt(Instant.now())
.build();
return repository.create(resource);
});
}
}
```
### Error Handling
- **Specific Exceptions**: Use Jakarta exceptions (BadRequestException, NotFoundException, etc.)
- **Graceful Handling**: Always handle exceptions gracefully
- **Logging**: Use SLF4J with `@Slf4j` annotation
- **Context**: Include relevant context in log messages (surround values with single quotes)
**Example Error Handling:**
```java
@Slf4j
public class ResourceService {
public ResourceResponse getResource(String id) {
try {
return resourceDao.findById(id)
.orElseThrow(() -> new NotFoundException("Resource not found: '%s'".formatted(id)));
} catch (SQLException exception) {
log.error("Database error while retrieving resource: '{}'", id, exception);
throw new InternalServerErrorException("Failed to retrieve resource", exception);
}
}
}
```
### Database Migrations
- **MySQL**: Place in `src/main/resources/liquibase/db-app-state/migrations/`
- **ClickHouse**: Place in `src/main/resources/liquibase/db-app-analytics/migrations/`
- **Format**: Include `--liquibase formatted sql` and proper changeset metadata
- **Indexes**: Add only relevant indexes with explanatory comments
### Testing Requirements
- **Unit Tests**: Test business logic with mocks
- **Integration Tests**: Test component interactions
- **Test Data**: Use PODAM for generating test data
- **Naming**: Follow camelCase conventions for test methods
### Code Quality Standards
- **File Formatting**: All files must end with a blank line
- **Naming**: Use meaningful variable and method names
- **Collections**: Prefer `Map.of()`, `List.of()`, `Set.of()` for immutable collections
- **List Access**: Use `getFirst()` or `getLast()` instead of `get(0)` or `get(size() - 1)`
- **Constants**: Replace magic numbers with named constants
- **Documentation**: Use Javadoc for public methods and classes
---
## 3. Frontend (React/TypeScript) Review Guidelines
### Technology Stack
- **Language**: TypeScript 5.4.5
- **Framework**: React 18.3.1
- **Build Tool**: Vite 5.2.11
- **Styling**: Tailwind CSS 3.4.3
- **State Management**: Zustand 4.5.2
- **Testing**: Vitest 3.0.5, Playwright 1.45.3
### Component Development Patterns
- **Performance Optimization**: Always use `useMemo` for data transformations and `useCallback` for event handlers
- **Component Structure**: Follow established patterns with proper TypeScript interfaces
- **UI Components**: Use shadcn/ui components with consistent variants
- **Styling**: Use Tailwind CSS with custom design system classes
**Example Component Pattern:**
```typescript
import React, { useMemo, useCallback } from "react";
import { cn } from "@/lib/utils";
type ComponentProps = {
// Props interface
};
const Component: React.FunctionComponent<ComponentProps> = ({
prop1,
prop2,
...props
}) => {
// 1. State hooks
// 2. useMemo for expensive computations
// 3. useCallback for event handlers
// 4. Other hooks
const processedData = useMemo(() => transformData(rawData), [rawData]);
const handleClick = useCallback(() => {}, [deps]);
return (
<div className="component-container">
{/* JSX */}
</div>
);
};
```
### Data Fetching Patterns
- **React Query**: Use TanStack Query for data fetching and caching
- **Query Keys**: Use descriptive keys with proper parameters
- **Error Handling**: Implement proper error states and loading indicators
- **Optimistic Updates**: Use mutations for data updates
### State Management
- **Zustand**: Use for global state management
- **Local Storage**: Use `use-local-storage-state` for persistence
- **Selectors**: Create focused selectors for state access
### Form Handling
- **React Hook Form**: Use with Zod validation
- **Validation**: Implement comprehensive form validation
- **Error Display**: Show validation errors clearly
### Testing Patterns
- **Unit Tests**: Test individual components and hooks
- **Integration Tests**: Test component interactions
- **E2E Tests**: Test complete user workflows with Playwright
- **Test Data**: Use realistic test data and proper mocking
### UI Component Patterns
- **Button Variants**: Use established variant system (default, secondary, outline, destructive, ghost, minimal)
- **Data Tables**: Use DataTable wrapper with proper column definitions
- **Loading States**: Use Skeleton components for loading states
- **Error States**: Use proper error styling with destructive colors
### Styling Guidelines
- **Design System**: Use custom CSS properties and typography classes
- **Color System**: Use semantic color classes (primary, secondary, muted, destructive)
- **Layout Classes**: Use consistent spacing and sizing patterns
- **Responsive Design**: Use Tailwind responsive prefixes appropriately
---
## 4. Python SDK Review Guidelines
### Technology Stack
- **Language**: Python 3.8+
- **Package Manager**: setuptools with pyproject.toml
- **HTTP Client**: httpx
- **Validation**: Pydantic 2.x
- **Testing**: pytest
### API Design Principles
- **Main API Class**: `opik.Opik` is the main entry point
- **Higher Level APIs**: Provide wrappers for complex REST calls
- **Backward Compatibility**: Maintain compatibility for public interfaces
- **Consistency**: Follow existing API patterns
### Architecture Patterns
- **Layered Architecture**: API Objects → Message Processing → REST API → Backend
- **Non-blocking Operations**: Create spans, traces, and feedback scores as background operations
- **Context Management**: Use `opik.opik_context` and `@opik.track` decorator
- **Integration Patterns**: Extend base decorator classes for new integrations
### Code Organization
- **Import Organization**: Import modules, not names (except from `typing`)
- **Access Control**: Use proper access modifiers (protected methods with underscores)
- **Module Structure**: Organize by functionality, avoid generic utility modules
- **Naming**: Use meaningful names that reflect purpose
### Dependency Management
- **Existing Dependencies**: Prioritize keeping existing dependencies
- **Version Bounds**: Use flexible version bounds with appropriate constraints
- **Conditional Imports**: Use for optional dependencies (integrations)
- **Python Versions**: Ensure compatibility with specified Python versions
### Error Handling
- **Specific Exceptions**: Use specific exception types for different error categories
- **Structured Errors**: Use consistent structured error information
- **Recovery Logic**: Implement proper retry logic for transient failures
- **Provider Errors**: Handle provider-specific errors in integrations
### Testing Requirements
- **Test Naming**: Use convention `test_WHAT__CASE_DESCRIPTION__EXPECTED_RESULT`
- **Test Organization**: Unit tests, library integration tests, end-to-end tests
- **Test Data**: Use `fake_backend` fixture for emulating real backend
- **Coverage**: Test public API only, never violate privacy
### Logging Guidelines
- **Structured Logging**: Use proper logger hierarchies
- **Log Levels**: DEBUG for detailed info, INFO/WARNING for user messages, ERROR for problems
- **Context**: Include relevant context without exposing sensitive information
- **Timing**: Include timing information for API calls and processing
---
## 5. TypeScript SDK Review Guidelines
### Technology Stack
- **Language**: TypeScript 5.7.2
- **Runtime**: Node.js 18+
- **Build Tool**: tsup 8.3.6
- **HTTP Client**: node-fetch 3.3.2
- **Validation**: Zod 3.25.55
### Code Quality Standards
- **Type Safety**: Use comprehensive TypeScript types
- **ES Modules**: Use modern ES module syntax
- **Error Handling**: Implement proper error handling with typed errors
- **Documentation**: Include comprehensive JSDoc comments
### Testing Patterns
- **Unit Tests**: Test individual functions and classes
- **Integration Tests**: Test API interactions
- **Mocking**: Use proper mocking for external dependencies
- **Type Testing**: Test TypeScript types and interfaces
---
## 6. General Code Quality Guidelines
### Clean Code Principles
- **Constants**: Replace magic numbers with named constants
- **Meaningful Names**: Variables, functions, and classes should reveal their purpose
- **Single Responsibility**: Each function should do exactly one thing
- **DRY**: Don't repeat yourself - extract common logic
- **Comments**: Explain why, not what - make code self-documenting
### Performance Considerations
- **Efficient Algorithms**: Use appropriate data structures and algorithms
- **Memory Management**: Avoid memory leaks and excessive allocations
- **Database Optimization**: Use proper indexes and query optimization
- **Caching**: Implement appropriate caching strategies
### Security Guidelines
- **Input Validation**: Validate all external inputs
- **Authentication**: Implement proper authentication and authorization
- **Data Protection**: Never log sensitive information (PII, credentials)
- **Dependency Security**: Keep dependencies updated and scan for vulnerabilities
### Documentation Standards
- **API Documentation**: Use OpenAPI/Swagger for backend APIs
- **Code Comments**: Use Javadoc, JSDoc, or docstrings as appropriate
- **README Files**: Keep documentation up to date
- **Examples**: Provide usage examples for complex functionality
---
## 7. Testing Guidelines
### Test Organization
- **Unit Tests**: Fast, isolated, no external dependencies
- **Integration Tests**: Test component interactions
- **E2E Tests**: Test complete user workflows
- **Performance Tests**: Load and stress testing where applicable
### Test Quality Standards
- **Coverage**: Aim for comprehensive test coverage
- **Readability**: Tests should be easy to understand and maintain
- **Reliability**: Tests should be deterministic and not flaky
- **Performance**: Tests should run quickly and efficiently
### Test Data Management
- **Realistic Data**: Use realistic but not sensitive test data
- **Fixtures**: Use test fixtures for common setup
- **Isolation**: Each test should be independent
- **Cleanup**: Properly clean up test data and resources
### Backend Testing (Java)
- **PODAM**: Use for generating test data with `PodamFactoryUtils.newPodamFactory()`
- **Naming**: Follow camelCase conventions (`shouldCreateUser_whenValidRequest`)
- **Assertions**: Use AssertJ for fluent assertions
- **Mocking**: Use Mockito for mocking dependencies
### Frontend Testing (TypeScript)
- **React Testing Library**: Use for component testing
- **MSW**: Use for API mocking
- **Playwright**: Use for E2E testing
- **Vitest**: Use for unit testing
### Python SDK Testing
- **pytest**: Use for all testing
- **fake_backend**: Use fixture for backend emulation
- **Test Naming**: Use descriptive test names with underscores
- **Coverage**: Test public API only
---
## 8. Dependency Management
### Version Strategy
- **Pin Major Versions**: For production stability
- **Allow Minor Updates**: For security patches and bug fixes
- **Security Updates**: Automate security patch updates
- **Breaking Changes**: Test thoroughly before major version upgrades
### Dependency Guidelines
- **Existing Dependencies**: Prefer existing dependencies over adding new ones
- **Security**: Keep dependencies updated and scan for vulnerabilities
- **Licensing**: Ensure all dependencies have acceptable licenses
- **Size**: Consider the impact of adding new dependencies
### Technology-Specific Dependencies
#### Backend (Java)
- **Core**: Dropwizard 5.0.0, JDBI3, MySQL 9.7.0, ClickHouse 0.9.0
- **Build**: Maven, Spotless 3.5.1
- **Testing**: JUnit 5, Testcontainers, WireMock
- **Observability**: OpenTelemetry 2.28.1
#### Frontend (TypeScript)
- **Core**: React 18.3.1, TypeScript 5.4.5, Vite 5.2.11
- **UI**: Tailwind CSS 3.4.3, shadcn/ui, Radix UI
- **State**: Zustand 4.5.2, TanStack Query 5.45.0
- **Testing**: Vitest 3.0.5, Playwright 1.45.3
#### Python SDK
- **Core**: Python 3.8+, httpx, Pydantic 2.x
- **Testing**: pytest
- **CLI**: Click
- **Logging**: Rich, Sentry SDK
#### TypeScript SDK
- **Core**: TypeScript 5.7.2, Node.js 18+, tsup 8.3.6
- **HTTP**: node-fetch 3.3.2
- **Validation**: Zod 3.25.55
- **Logging**: tslog 4.9.3
---
## 9. Review Checklist
### Before Review
- [ ] Understand the context and purpose of the changes
- [ ] Check if the changes follow established patterns
- [ ] Verify that tests are included and appropriate
- [ ] Ensure documentation is updated if needed
### During Review
- [ ] Check code quality and adherence to standards
- [ ] Verify error handling and edge cases
- [ ] Review performance implications
- [ ] Check security considerations
- [ ] Ensure proper logging and observability
- [ ] Verify test coverage and quality
### After Review
- [ ] Provide constructive feedback
- [ ] Suggest improvements when appropriate
- [ ] Approve only when standards are met
- [ ] Follow up on any issues identified
---
## 10. Common Issues to Watch For
### Backend Issues
- Missing transaction boundaries
- Improper exception handling
- Missing validation annotations
- Inconsistent logging patterns
- Missing or incorrect API documentation
- Not using `@Slf4j` annotation
- Logging sensitive information
- Not surrounding logged values with single quotes
### Frontend Issues
- Missing performance optimizations (useMemo, useCallback)
- Improper error handling
- Missing loading states
- Inconsistent component patterns
- Missing accessibility features
- Not using proper TypeScript types
- Inline functions in JSX props
### SDK Issues
- Breaking API changes without proper deprecation
- Missing error handling
- Inconsistent naming conventions
- Missing documentation
- Improper dependency management
- Not following import organization rules
### General Issues
- Code duplication
- Magic numbers or hardcoded values
- Missing tests
- Poor error messages
- Security vulnerabilities
- Performance issues
- Files not ending with blank lines
- Inconsistent naming conventions
---
## 11. Technology-Specific Review Focus Areas
### Backend (Java) Focus
- **Architecture**: Layered architecture compliance
- **Transactions**: Proper TransactionTemplate usage
- **Validation**: Jakarta validation annotations
- **Logging**: SLF4J with proper context
- **Testing**: PODAM usage and test naming
- **Database**: Migration script quality
- **Error Handling**: Specific exception types
### Frontend (TypeScript) Focus
- **Performance**: useMemo and useCallback usage
- **TypeScript**: Proper type definitions
- **Components**: shadcn/ui patterns
- **Styling**: Tailwind CSS conventions
- **State Management**: Zustand patterns
- **Testing**: Component and E2E test coverage
- **Accessibility**: ARIA labels and semantic HTML
### Python SDK Focus
- **API Design**: Main Opik class usage
- **Architecture**: Layered patterns
- **Testing**: Test naming conventions
- **Logging**: Structured logging
- **Dependencies**: Minimal dependency addition
- **Documentation**: Comprehensive docstrings
### TypeScript SDK Focus
- **Type Safety**: Comprehensive TypeScript usage
- **ES Modules**: Modern module syntax
- **Error Handling**: Typed error handling
- **Documentation**: JSDoc comments
- **Testing**: Unit and integration tests
---
Use these guidelines to provide comprehensive, consistent, and helpful code review feedback across all Opik applications. Each section provides specific, actionable guidance for the technology stack being reviewed.
+15
View File
@@ -0,0 +1,15 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "maven"
directory: "apps/opik-backend"
schedule:
interval: "weekly" # How often to check for updates
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-patch"]
open-pull-requests-limit: 5
File diff suppressed because it is too large Load Diff
+83
View File
@@ -0,0 +1,83 @@
Frontend:
- changed-files:
- any-glob-to-any-file: 'apps/opik-frontend/**/*'
Backend:
- changed-files:
- any-glob-to-any-file:
- 'apps/opik-backend/**/*'
- 'apps/opik-guardrails-backend/**/*'
- 'apps/opik-python-backend/**/*'
- 'apps/opik-sandbox-executor-python/**/*'
Python SDK:
- changed-files:
- any-glob-to-any-file: 'sdks/python/**/*'
TypeScript SDK:
- changed-files:
- any-glob-to-any-file: 'sdks/typescript/**/*'
Optimizer SDK:
- changed-files:
- any-glob-to-any-file: 'sdks/opik_optimizer/**/*'
python:
- changed-files:
- any-glob-to-any-file: '**/*.py'
java:
- changed-files:
- any-glob-to-any-file: '**/*.java'
typescript:
- changed-files:
- any-glob-to-any-file:
- '**/*.ts'
- '**/*.tsx'
tests:
- changed-files:
- any-glob-to-any-file:
- 'apps/opik-backend/src/test/**/*'
- 'apps/opik-guardrails-backend/tests/**/*'
- 'apps/opik-python-backend/tests/**/*'
- 'apps/opik-frontend/e2e/**/*'
- 'sdks/python/tests/**/*'
- 'sdks/opik_optimizer/tests/**/*'
- 'sdks/typescript/tests/**/*'
- 'tests_end_to_end/**/*'
- 'tests_load/**/*'
dependencies:
- changed-files:
- any-glob-to-any-file:
- '**/pom.xml'
- '**/package.json'
- '**/package-lock.json'
- '**/requirements.txt'
- '**/pyproject.toml'
- '**/setup.py'
- '**/.python-version'
- '**/.java-version'
Infrastructure:
- changed-files:
- any-glob-to-any-file:
- '.github/**/*'
- '.hooks/**/*'
- 'deployment/**/*'
- 'scripts/**/*'
- 'extensions/**/*'
- '**/Dockerfile'
- '**/.gitignore'
- '**/*.sh'
- '**/*.ps1'
documentation:
- changed-files:
- any-glob-to-any-file:
- 'apps/opik-documentation/**/*'
- '**/*.mdc'
- '**/*.md'
- '**/*.mdx'
+51
View File
@@ -0,0 +1,51 @@
## Details
<!--
REPLACE ME WITH:
Short 2-3 lines single paragraph explaining whats changed and the motivation (why).
If you need to have changes explained in detail include single line dot point(s).
-->
## Change checklist
- [ ] User facing
- [ ] Documentation update
## Issues
- Resolves #
- OPIK-
<!--
Jira linking: tickets this PR RESOLVES keep the hyphen (OPIK-1234) here and anywhere — they link in Jira's Development panel, which is wanted. A PR may resolve more than one ticket; list each resolved key.
Tickets RELATED but NOT resolved here (escalations, references to older tickets) must NOT link: in the sections above/below and in commit messages, write them with an underscore (OPIK_7000) and paste no Jira URL (the URL contains the hyphenated key and links anyway). See CONTRIBUTING.md.
-->
## AI-WATERMARK
AI-WATERMARK: [yes|no]
- If yes:
- Tools:
- Model(s):
- Scope:
- Human verification:
## Testing
<!--
REPLACE ME WITH:
How you tested
Include:
- Exact commands run (for example: `mvn test`, `npm run lint && npm run test`, `pytest ...`)
- Scenarios validated (happy path, edge cases, regressions)
- Environment/context used (local process mode vs Docker, OS/browser when relevant)
- Any tests not run, with reason
- Relevant logs/artifacts links if available
Video evidence:
- External contributors: include a short recording for all contributions where possible.
- Internal contributors: include a short recording for complex changes where possible.
-->
## Documentation
+3
View File
@@ -0,0 +1,3 @@
template: |
## Whats Changed
$CHANGES
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
set -euo pipefail
NUM_GROUPS=16
UNIT_TIMEOUT=10
INTEGRATION_TIMEOUT=20
TEST_DIR="src/test/java"
PATTERN="DropwizardAppExtensionProvider\|MySQLContainer\|ClickHouseContainer\|RedisContainer\|MinIOContainer"
# Classify: integration tests reference container/Dropwizard patterns
integration_files=()
unit_files=()
while IFS= read -r file; do
if grep -ql "$PATTERN" "$file" 2>/dev/null; then
integration_files+=("$file")
else
unit_files+=("$file")
fi
done < <(find "$TEST_DIR" -name "*Test.java" -o -name "*Tests.java" | sort)
echo "Classified ${#unit_files[@]} unit test classes and ${#integration_files[@]} integration test classes"
# Convert file path to Maven class pattern: com.foo.BarTest
to_class() { echo "$1" | sed "s|^$TEST_DIR/||;s|/|.|g;s|\.java$||"; }
# Build unit test list
unit_list=""
for f in "${unit_files[@]}"; do
c=$(to_class "$f")
unit_list="${unit_list:+$unit_list,}$c"
done
# Balanced split: sort integration files by line count desc,
# then greedy-assign each to the smallest group
declare -a group_size group_list
for ((i=1; i<=NUM_GROUPS; i++)); do
group_size[$i]=0
group_list[$i]=""
done
while IFS=' ' read -r lines file; do
min_g=1
for ((i=2; i<=NUM_GROUPS; i++)); do
if (( group_size[i] < group_size[min_g] )); then
min_g=$i
fi
done
group_size[$min_g]=$(( group_size[min_g] + lines ))
c=$(to_class "$file")
group_list[$min_g]="${group_list[$min_g]:+${group_list[$min_g]},}$c"
done < <(
for f in "${integration_files[@]}"; do
echo "$(wc -l < "$f") $f"
done | sort -rn
)
echo "Balanced ${#integration_files[@]} integration classes into $NUM_GROUPS groups"
for ((i=1; i<=NUM_GROUPS; i++)); do
count=$(echo "${group_list[$i]}" | tr ',' '\n' | grep -c '.' || true)
echo " Group $i: $count classes (~${group_size[$i]} lines)"
done
# Build JSON matrix: unit tests + N integration groups
matrix="{\"include\":["
matrix+="{\"name\":\"Unit Tests\",\"tests\":\"$unit_list\",\"timeout\":$UNIT_TIMEOUT}"
for ((i=1; i<=NUM_GROUPS; i++)); do
matrix+=",{\"name\":\"Integration Group $i\",\"tests\":\"${group_list[$i]}\",\"timeout\":$INTEGRATION_TIMEOUT}"
done
matrix+="]}"
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
echo "Matrix written to GITHUB_OUTPUT ($(( NUM_GROUPS + 1 )) jobs: 1 unit + $NUM_GROUPS integration)"
# Summary
echo "### Test Split Summary" >> "$GITHUB_STEP_SUMMARY"
echo "- **Unit tests**: ${#unit_files[@]} classes" >> "$GITHUB_STEP_SUMMARY"
echo "- **Integration tests**: ${#integration_files[@]} classes in $NUM_GROUPS groups" >> "$GITHUB_STEP_SUMMARY"
for ((i=1; i<=NUM_GROUPS; i++)); do
count=$(echo "${group_list[$i]}" | tr ',' '\n' | grep -c '.' || true)
echo " - Group $i: $count classes (~${group_size[$i]} lines)" >> "$GITHUB_STEP_SUMMARY"
done
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
#
# Build and send the E2E post-merge Slack notification.
#
# Required env vars:
# SLACK_WEBHOOK_URL — Slack incoming webhook URL
# TEST_RESULT — "success" or "failure"
# SUITE_NAME — test suite name (e.g. "happypaths")
# PASSED_TESTS — count of passed tests
# FAILED_TESTS — count of failed tests
# TESTOPS_URL — link to TestOps dashboard
# AUTHOR_DISPLAY — GitHub username of the PR author
# MENTIONS — pre-resolved Slack mentions (e.g. "<@U123> <@U456>"), may be empty
# GITHUB_REF_NAME — branch name (set by GitHub Actions)
# GITHUB_SHA — commit SHA (set by GitHub Actions)
# GITHUB_REPOSITORY — owner/repo (set by GitHub Actions)
# GITHUB_RUN_ID — workflow run ID (set by GitHub Actions)
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SHORT_SHA="${GITHUB_SHA:0:7}"
WORKFLOW_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
if [ "$TEST_RESULT" == "success" ]; then
STATUS_EMOJI="\u2705"
STATUS_TEXT="Passed"
COLOR="good"
else
STATUS_EMOJI="\u274c"
STATUS_TEXT="Failed"
COLOR="danger"
fi
MENTION_BLOCK=""
if [ "$TEST_RESULT" == "failure" ] && [ -n "${MENTIONS:-}" ]; then
MENTION_BLOCK=',
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "\ud83d\udea8 '"${MENTIONS}"' - Tests need attention!"
}
}'
fi
cat << EOF > /tmp/slack-payload.json
{
"attachments": [
{
"color": "$COLOR",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "$STATUS_EMOJI E2E Tests $STATUS_TEXT - Post Merge",
"emoji": true
}
}$MENTION_BLOCK,
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": "*Suite:*\n\`${SUITE_NAME}\`"},
{"type": "mrkdwn", "text": "*Branch:*\n\`${GITHUB_REF_NAME}\`"},
{"type": "mrkdwn", "text": "*Results:*\n\u2705 ${PASSED_TESTS} passed, \u274c ${FAILED_TESTS} failed"},
{"type": "mrkdwn", "text": "*Commit:*\n<https://github.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}|\`${SHORT_SHA}\`>"}
]
},
{
"type": "context",
"elements": [
{"type": "mrkdwn", "text": "\ud83d\udc64 *Author:* ${AUTHOR_DISPLAY}"}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": "\ud83d\udcca TestOps", "emoji": true},
"url": "${TESTOPS_URL}",
"style": "primary"
},
{
"type": "button",
"text": {"type": "plain_text", "text": "\ud83d\udd0d Workflow", "emoji": true},
"url": "${WORKFLOW_URL}"
}
]
}
]
}
]
}
EOF
exec "$SCRIPT_DIR/send-slack-message.sh" /tmp/slack-payload.json
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
#
# Build and send the library integration tests Slack notification.
#
# Required env vars:
# SLACK_WEBHOOK_URL — Slack incoming webhook URL
# SUITE_RESULTS — JSON object mapping suite display names to results
# e.g. '{"OpenAI":"success","LangChain":"failure"}'
# TRIGGER_TYPE — "Daily Schedule", "Weekly Schedule", or "Manual Dispatch"
# GITHUB_REF_NAME — branch name (set by GitHub Actions)
# GITHUB_SHA — commit SHA (set by GitHub Actions)
# GITHUB_REPOSITORY — owner/repo (set by GitHub Actions)
# GITHUB_RUN_ID — workflow run ID (set by GitHub Actions)
#
# Optional env vars:
# SLACK_USER_ID — Slack user ID to cc on the notification
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SHORT_SHA="${GITHUB_SHA:0:7}"
WORKFLOW_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
COMMIT_URL="https://github.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}"
# Categorize suites by result
SUCCESS_SUITES=""
FAILED_SUITES=""
SKIPPED_SUITES=""
SUCCESS_COUNT=0
FAILURE_COUNT=0
SKIPPED_COUNT=0
while IFS=$'\t' read -r name result; do
case "$result" in
success)
[ -n "$SUCCESS_SUITES" ] && SUCCESS_SUITES="${SUCCESS_SUITES}, "
SUCCESS_SUITES="${SUCCESS_SUITES}${name}"
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
;;
failure)
[ -n "$FAILED_SUITES" ] && FAILED_SUITES="${FAILED_SUITES}, "
FAILED_SUITES="${FAILED_SUITES}${name}"
FAILURE_COUNT=$((FAILURE_COUNT + 1))
;;
*)
[ -n "$SKIPPED_SUITES" ] && SKIPPED_SUITES="${SKIPPED_SUITES}, "
SKIPPED_SUITES="${SKIPPED_SUITES}${name}"
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
;;
esac
done < <(echo "$SUITE_RESULTS" | jq -r 'to_entries[] | "\(.key)\t\(.value)"')
if [ "$FAILURE_COUNT" -gt 0 ]; then
COLOR="danger"
STATUS_TEXT="Failed"
else
COLOR="good"
STATUS_TEXT="Passed"
fi
# Build blocks with jq
BLOCKS='[]'
BLOCKS=$(echo "$BLOCKS" | jq --arg title "Python SDK Integration Tests $STATUS_TEXT" \
'. + [{"type": "header", "text": {"type": "plain_text", "text": $title, "emoji": true}}]')
BLOCKS=$(echo "$BLOCKS" | jq \
--arg trigger "$TRIGGER_TYPE" \
--arg branch "$GITHUB_REF_NAME" \
--arg commit_url "$COMMIT_URL" \
--arg short_sha "$SHORT_SHA" \
'. + [{"type": "section", "fields": [
{"type": "mrkdwn", "text": ("*Trigger:*\n" + $trigger)},
{"type": "mrkdwn", "text": ("*Branch:*\n`" + $branch + "`")},
{"type": "mrkdwn", "text": ("*Commit:*\n<" + $commit_url + "|`" + $short_sha + "`>")}
]}]')
BLOCKS=$(echo "$BLOCKS" | jq \
--arg s "$SUCCESS_COUNT" --arg f "$FAILURE_COUNT" --arg k "$SKIPPED_COUNT" \
'. + [{"type": "section", "text": {"type": "mrkdwn",
"text": ("\u2705 *Passed:* " + $s + " | \u274c *Failed:* " + $f + " | \u23ed\ufe0f *Skipped:* " + $k)}}]')
if [ -n "$FAILED_SUITES" ]; then
BLOCKS=$(echo "$BLOCKS" | jq --arg suites "$FAILED_SUITES" \
'. + [{"type": "section", "text": {"type": "mrkdwn", "text": ("\u274c *Failed:* " + $suites)}}]')
fi
if [ -n "$SUCCESS_SUITES" ]; then
BLOCKS=$(echo "$BLOCKS" | jq --arg suites "$SUCCESS_SUITES" \
'. + [{"type": "section", "text": {"type": "mrkdwn", "text": ("\u2705 *Passed:* " + $suites)}}]')
fi
if [ -n "$SKIPPED_SUITES" ]; then
BLOCKS=$(echo "$BLOCKS" | jq --arg suites "$SKIPPED_SUITES" \
'. + [{"type": "section", "text": {"type": "mrkdwn", "text": ("\u23ed\ufe0f *Skipped:* " + $suites)}}]')
fi
BLOCKS=$(echo "$BLOCKS" | jq --arg url "$WORKFLOW_URL" \
'. + [{"type": "actions", "elements": [{"type": "button", "text": {"type": "plain_text", "text": "\ud83d\udd0d View Workflow", "emoji": true}, "url": $url}]}]')
if [ -n "${SLACK_USER_ID:-}" ]; then
BLOCKS=$(echo "$BLOCKS" | jq --arg uid "$SLACK_USER_ID" \
'. + [{"type": "context", "elements": [{"type": "mrkdwn", "text": ("\ud83d\udc64 cc: <@" + $uid + ">")}]}]')
fi
echo "$BLOCKS" | jq --arg color "$COLOR" '{"attachments": [{"color": $color, "blocks": .}]}' > /tmp/slack-payload.json
echo "Payload:"
cat /tmp/slack-payload.json
exec "$SCRIPT_DIR/send-slack-message.sh" /tmp/slack-payload.json
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
#
# Build and send a simple test-failure Slack notification.
#
# Required env vars:
# SLACK_WEBHOOK_URL — Slack incoming webhook URL
# GITHUB_REF_NAME — branch name (set by GitHub Actions)
# GITHUB_SHA — commit SHA (set by GitHub Actions)
# GITHUB_REPOSITORY — owner/repo (set by GitHub Actions)
# GITHUB_RUN_ID — workflow run ID (set by GitHub Actions)
# GITHUB_EVENT_NAME — trigger event (set by GitHub Actions)
# GITHUB_ACTOR — user who triggered the workflow (set by GitHub Actions)
#
# Usage:
# notify-slack-test-failure.sh "Guardrails E2E Tests"
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
TEST_NAME="${1:?Usage: notify-slack-failure.sh <test-name>}"
SHORT_SHA="${GITHUB_SHA:0:7}"
WORKFLOW_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
cat << EOF > /tmp/slack-payload.json
{
"attachments": [
{
"color": "danger",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "\u274c ${TEST_NAME} Failed",
"emoji": true
}
},
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": "*Branch:*\n\`${GITHUB_REF_NAME}\`"},
{"type": "mrkdwn", "text": "*Trigger:*\n\`${GITHUB_EVENT_NAME}\`"},
{"type": "mrkdwn", "text": "*Author:*\n${GITHUB_ACTOR}"},
{"type": "mrkdwn", "text": "*Commit:*\n<https://github.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}|\`${SHORT_SHA}\`>"}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": "\ud83d\udd0d View Workflow", "emoji": true},
"url": "${WORKFLOW_URL}",
"style": "primary"
}
]
}
]
}
]
}
EOF
exec "$SCRIPT_DIR/send-slack-message.sh" /tmp/slack-payload.json
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
#
# Send a JSON payload to a Slack webhook.
#
# Required env vars:
# SLACK_WEBHOOK_URL — Slack incoming webhook URL
#
# Usage:
# send-slack-message.sh payload.json
# build-payload | send-slack-message.sh /dev/stdin
#
set -euo pipefail
PAYLOAD_FILE="${1:?Usage: send-slack-message.sh <payload-file>}"
if [ -z "${SLACK_WEBHOOK_URL:-}" ]; then
echo "::notice::SLACK_WEBHOOK_URL not configured - Slack notification will be skipped"
exit 0
fi
if [ ! -f "$PAYLOAD_FILE" ] && [ "$PAYLOAD_FILE" != "/dev/stdin" ]; then
echo "::error::Payload file not found: $PAYLOAD_FILE"
exit 1
fi
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST -H 'Content-type: application/json' \
--data @"$PAYLOAD_FILE" "$SLACK_WEBHOOK_URL")
if [ "$HTTP_CODE" -eq 200 ]; then
echo "Slack notification sent"
else
echo "::warning::Slack notification failed with HTTP code: $HTTP_CODE"
exit 1
fi
+91
View File
@@ -0,0 +1,91 @@
name: "Backend Tests"
run-name: "Backend Tests on ${{ github.ref_name }} by @${{ github.actor }}"
on:
pull_request:
paths:
- "apps/opik-backend/**"
- ".github/workflows/backend_tests.yml"
- ".github/scripts/discover-backend-tests.sh"
push:
branches:
- 'main'
paths:
- "apps/opik-backend/**"
- ".github/workflows/backend_tests.yml"
- ".github/scripts/discover-backend-tests.sh"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
checks: write
pull-requests: write
jobs:
# Automatically classify tests as unit/integration and split integration
# tests into balanced groups. No manual maintenance when adding new tests.
discover-tests:
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
matrix: ${{ steps.split.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 1
sparse-checkout: |
apps/opik-backend/src/test/java
.github/scripts
- name: Classify and split tests
id: split
working-directory: apps/opik-backend
run: ../../.github/scripts/discover-backend-tests.sh
run-backend-tests:
needs: discover-tests
name: ${{ matrix.name }}
runs-on: ubuntu-latest
timeout-minutes: ${{ matrix.timeout }}
defaults:
run:
working-directory: apps/opik-backend/
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.discover-tests.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'corretto'
cache: maven
- name: Run ${{ matrix.name }}
env:
TESTCONTAINERS_REUSE_ENABLE: true
run: >-
mvn clean test
-Dtest="${{ matrix.tests }}"
-Dmaven.test.failure.ignore=true
-Dsurefire.rerunFailingTestsCount=3
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action/linux@v2
if: always()
with:
action_fail: true
comment_mode: failures
check_name: "Backend Tests - ${{ matrix.name }}"
files: '**/target/surefire-reports/TEST-*.xml'
@@ -0,0 +1,81 @@
name: "Build and Publish SDK"
run-name: "Build SDK ${{ github.ref_name }} by @${{ github.actor }}"
on:
workflow_dispatch:
inputs:
version:
type: string
required: true
description: Version
default: ""
is_release:
type: boolean
required: false
default: false
workflow_call:
inputs:
version:
type: string
required: true
description: Version
is_release:
type: boolean
required: false
default: false
push:
branches:
- 'main'
paths:
- "sdks/python/**"
jobs:
build-and-publish:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Setup
id: setup
run: |
if [[ "${{inputs.version}}" == "" ]]; then
echo "build_from=${{github.ref_name}}" | tee -a "$GITHUB_OUTPUT"
else
echo "build_from=${{inputs.version}}" | tee -a "$GITHUB_OUTPUT"
fi
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{steps.setup.outputs.build_from}}
fetch-tags: true
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: PyPI token preflight
if: inputs.is_release
uses: ./.github/actions/pypi-token-preflight
with:
pypi_token: ${{ secrets.PYPI_API_TOKEN }}
package_name: opik
- name: Build pip package
run: |
cd sdks/python
pip3 install -U pip build
if [[ "${{inputs.version}}" != "" ]]; then export VERSION=${{inputs.version}};fi
python3 -m build --sdist --wheel --outdir dist/ .
- name: Publish package distributions to PyPI
if: inputs.is_release
uses: pypa/gh-action-pypi-publish@v1.12.4
with:
password: ${{ secrets.PYPI_API_TOKEN }}
packages-dir: sdks/python/dist
# Symmetric to OPIK-6624's npm publish 403-skip: replays / races where
# the version is already on PyPI should not fail the release. PyPI returns
# `400 Bad Request` (filename already exists) — `skip-existing: true`
# makes twine treat it as a no-op success.
skip-existing: true
@@ -0,0 +1,310 @@
name: "Build Docker Images Multi-Arch"
run-name: "Build ${{ inputs.image }}${{ inputs.build_comet_image && '-comet' || '' }} - ${{ inputs.version }}"
on:
workflow_call:
inputs:
image:
type: string
required: true
description: Docker Image
version:
type: string
required: true
description: Version
build_from:
type: string
required: true
description: Original version to build from
build_comet_image:
type: boolean
required: true
description: If to build a Comet integration image
comet_build_args:
type: string
required: false
default: ""
description: Arguments for cloud docker build
is_release:
type: boolean
required: true
description: If this is a release build (will build multi-arch and push latest tag)
is_adhoc:
type: boolean
required: false
description: If this is an adhoc build (will skip comet variant)
default: false
build_arm64:
type: boolean
required: false
description: Build multiarch images
default: true
ai_spend_plugin_ref:
type: string
required: false
default: "main"
description: ai-spend plugin ref
secrets:
OPIK_FE_SENTRY_DSN:
required: false
OPIK_PLUGIN_AI_SPEND_TOKEN:
required: false
env:
DOCKER_REGISTRY: "ghcr.io/comet-ml/opik"
DOCKER_BUILD_SUMMARY: "false"
jobs:
generate-matrix:
runs-on: ubuntu-latest
outputs:
build_matrix: ${{ steps.set-matrix.outputs.build_matrix }}
merge_matrix: ${{ steps.set-matrix.outputs.merge_matrix }}
steps:
- name: Generate build and merge matrices
id: set-matrix
run: |
# Build 2D matrix: image_type x platform
BUILD_MATRIX='{"image_type":["regular","comet"],"platform":["amd64","arm64"],"exclude":[]}'
MERGE_MATRIX='{"image_type":["regular","comet"],"exclude":[]}'
# Exclude comet if build_comet_image is false
if [[ "${{ inputs.build_comet_image }}" != "true" ]]; then
BUILD_MATRIX=$(echo "$BUILD_MATRIX" | jq '.exclude += [{"image_type":"comet"}]')
MERGE_MATRIX=$(echo "$MERGE_MATRIX" | jq '.exclude += [{"image_type":"comet"}]')
fi
# Exclude arm64 when the caller opts out, or for guardrails backend
if [[ "${{ inputs.build_arm64 }}" != "true" ]] || [[ "${{ inputs.image }}" == "opik-guardrails-backend" ]]; then
BUILD_MATRIX=$(echo "$BUILD_MATRIX" | jq '.exclude += [{"platform":"arm64"}]')
fi
{
echo "build_matrix<<EOF"
echo "$BUILD_MATRIX"
echo "EOF"
echo "merge_matrix<<EOF"
echo "$MERGE_MATRIX"
echo "EOF"
} >> "$GITHUB_OUTPUT"
build:
needs: generate-matrix
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs.build_matrix) }}
runs-on: ${{ inputs.image == 'opik-guardrails-backend' && matrix.platform == 'amd64' && 'ubuntu-latest-m' || (matrix.platform == 'amd64' && 'ubuntu-latest' || 'ubuntu-24.04-arm') }}
name: ${{ matrix.platform }} ${{ inputs.image }}${{ matrix.image_type == 'comet' && '-comet' || '' }}
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.build.outputs.digest }}
platform: ${{ matrix.platform }}
image_name: ${{ steps.set_vars.outputs.image_name }}
timeout-minutes: 60
steps:
- name: Set image variables
id: set_vars
run: |
if [[ "${{ matrix.image_type }}" == "comet" ]]; then
IMAGE_NAME="${{ inputs.image }}-comet"
else
IMAGE_NAME="${{ inputs.image }}"
fi
echo "image_name=$IMAGE_NAME" >> "$GITHUB_OUTPUT"
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.build_from }}
- name: Save opik-sandbox-executor-python
if: inputs.image == 'opik-python-backend'
run: |
# Use latest tag if adhoc build, otherwise use the specific version
if [[ "${{ inputs.is_adhoc }}" == "true" ]]; then
TAG="latest"
else
TAG="${{inputs.version}}"
fi
docker pull "${{env.DOCKER_REGISTRY}}/opik-sandbox-executor-python:$TAG"
docker save "${{env.DOCKER_REGISTRY}}/opik-sandbox-executor-python:$TAG" | gzip > apps/opik-python-backend/opik-sandbox-executor-python.tar.gz
- name: Checkout ai-spend plugin (private)
if: inputs.image == 'opik-frontend' && matrix.image_type == 'comet'
uses: actions/checkout@v6
with:
repository: comet-ml/opik-plugin-ai-spend
ref: ${{ inputs.ai_spend_plugin_ref }}
token: ${{ secrets.OPIK_PLUGIN_AI_SPEND_TOKEN }}
path: .ai-spend-plugin
- name: Stage ai-spend plugin into frontend src
if: inputs.image == 'opik-frontend' && matrix.image_type == 'comet'
run: |
mkdir -p apps/opik-frontend/src/plugins/ai-spend
cp -R .ai-spend-plugin/src/. apps/opik-frontend/src/plugins/ai-spend/
rm -rf .ai-spend-plugin
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKER_REGISTRY }}/${{ steps.set_vars.outputs.image_name }}
tags: |
type=raw,value=${{ inputs.version }}
type=raw,value=latest,enable=${{ inputs.is_release == true }}
type=raw,value=main,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and Push Docker Image
id: build
uses: docker/build-push-action@v6
with:
context: apps/${{ inputs.image }}/
platforms: linux/${{ matrix.platform }}
cache-from: type=registry,ref=${{ env.DOCKER_REGISTRY }}/${{ steps.set_vars.outputs.image_name }}:main
provenance: false
load: false
outputs: type=image,name=${{ env.DOCKER_REGISTRY }}/${{ steps.set_vars.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
labels: ${{ steps.meta.outputs.labels }}
build-args: |
OPIK_VERSION=${{ inputs.version }}
${{ matrix.image_type == 'comet' && inputs.comet_build_args }}
${{ inputs.image == 'opik-frontend' && matrix.image_type == 'comet' && 'BUILD_PLUGINS=comet,ai-spend' }}
${{ matrix.image_type == 'comet' && 'SENTRY_ENABLED=true' }}
${{ matrix.image_type == 'comet' && format('SENTRY_DSN={0}', secrets.OPIK_FE_SENTRY_DSN) }}
secrets: |
${{ matrix.image_type == 'comet' && secrets.OPIK_FE_SENTRY_DSN != '' && format('OPIK_FE_SENTRY_DSN={0}', secrets.OPIK_FE_SENTRY_DSN) || '' }}
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
- name: Export digest
run: |
mkdir -p /tmp/digests/${{ steps.set_vars.outputs.image_name }}
digest="${{ steps.build.outputs.digest }}"
# Strip sha256: prefix
HASH="${digest#sha256:}"
echo "$HASH" > "/tmp/digests/${{ steps.set_vars.outputs.image_name }}/${{ matrix.platform }}.digest"
- name: Upload digest
uses: actions/upload-artifact@v7
with:
name: ${{ github.run_id }}-${{ steps.set_vars.outputs.image_name }}-digest-${{ matrix.platform }}
path: /tmp/digests/${{ steps.set_vars.outputs.image_name }}/${{ matrix.platform }}.digest
if-no-files-found: error
retention-days: 1
merge:
needs: [generate-matrix, build]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix: ${{ fromJson(needs.generate-matrix.outputs.merge_matrix) }}
steps:
- name: Set image variables
id: set_vars
run: |
if [[ "${{ matrix.image_type }}" == "comet" ]]; then
IMAGE_NAME="${{ inputs.image }}-comet"
else
IMAGE_NAME="${{ inputs.image }}"
fi
echo "image_name=$IMAGE_NAME" >> "$GITHUB_OUTPUT"
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests/${{ steps.set_vars.outputs.image_name }}
pattern: ${{ github.run_id }}-${{ steps.set_vars.outputs.image_name }}-digest-*
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKER_REGISTRY }}/${{ steps.set_vars.outputs.image_name }}
tags: |
type=raw,value=${{ inputs.version }}
type=raw,value=latest,enable=${{ inputs.is_release == true }}
type=raw,value=main,enable=${{ github.ref == 'refs/heads/main' }}
- name: Create manifest list and push tags
run: |
cd /tmp/digests/${{ steps.set_vars.outputs.image_name }}
DIGESTS=""
for artifact_dir in ${{ github.run_id }}-${{ steps.set_vars.outputs.image_name }}-digest-*; do
if [ -d "$artifact_dir" ]; then
PLATFORM="${artifact_dir##*-}"
DIGEST_FILE="$artifact_dir/$PLATFORM.digest"
if [ -f "$DIGEST_FILE" ]; then
DIGEST_VALUE=$(tr -d '\n\r ' < "$DIGEST_FILE")
DIGEST_REF="${{ env.DOCKER_REGISTRY }}/${{ steps.set_vars.outputs.image_name }}@sha256:$DIGEST_VALUE"
DIGESTS="$DIGESTS $DIGEST_REF"
fi
fi
done
if [ -z "$DIGESTS" ]; then
echo "ERROR: No digests found for ${{ steps.set_vars.outputs.image_name }}"
exit 1
fi
# Create and push manifest using metadata tags
# Metadata outputs tags as newline-separated string, convert to -t flags
TAG_ARGS=""
while IFS= read -r tag; do
if [ -n "$tag" ]; then
TAG_ARGS="$TAG_ARGS -t $tag"
fi
done <<< "${{ steps.meta.outputs.tags }}"
if [ -z "$TAG_ARGS" ]; then
echo "ERROR: No tags generated from metadata!"
exit 1
fi
# $TAG_ARGS and $DIGESTS are intentionally unquoted: each holds a
# space-separated argument list (`-t tag1 -t tag2 ...` and a list of
# digest refs) that must word-split into individual argv entries.
# Quoting would collapse each into a single argument and break the
# docker invocation.
# shellcheck disable=SC2086
docker buildx imagetools create $TAG_ARGS $DIGESTS
- name: Write Build Summary
run: |
echo "### Docker images pushed: ${{ steps.set_vars.outputs.image_name }}" >> "$GITHUB_STEP_SUMMARY"
echo "${{ steps.meta.outputs.tags }}" >> "$GITHUB_STEP_SUMMARY"
if [[ "${{ inputs.image }}" == "opik-guardrails-backend" ]] || [[ "${{ inputs.build_arm64 }}" != "true" ]]; then
echo "Built for platforms: linux/amd64" >> "$GITHUB_STEP_SUMMARY"
else
echo "Built for platforms: linux/amd64, linux/arm64" >> "$GITHUB_STEP_SUMMARY"
fi
echo "- [View on GitHub Container Registry](https://github.com/${{ github.repository }}/pkgs/container/opik%2F${{ steps.set_vars.outputs.image_name }})" >> "$GITHUB_STEP_SUMMARY"
+238
View File
@@ -0,0 +1,238 @@
name: "Build Opik Docker Images"
run-name: "Build ${{ github.ref_name }} by @${{ github.actor }}"
on:
push:
branches:
- 'main'
paths-ignore:
- "deployment/**"
- 'sdks/**'
workflow_dispatch:
inputs:
is_release:
type: boolean
required: true
description: Is release build
default: false
is_adhoc:
type: boolean
required: false
description: Is adhoc build
default: false
build_guardrails:
type: boolean
required: true
description: Build guardrails backend
default: true
build_arm64:
type: boolean
required: false
description: Build multiarch images
default: true
ai_spend_plugin_ref:
type: string
required: false
description: ai-spend plugin ref
default: "main"
workflow_call:
inputs:
version:
type: string
required: true
description: Version
is_release:
type: boolean
required: true
description: Is release build
is_adhoc:
type: boolean
required: false
description: Is adhoc build
default: false
ref:
type: string
required: false
description: Git ref to build from (branch, tag, or commit SHA)
default: ""
build_guardrails:
type: boolean
required: false
description: Build guardrails backend
default: true
build_arm64:
type: boolean
required: false
description: Build multiarch images
default: true
ai_spend_plugin_ref:
type: string
required: false
description: ai-spend plugin ref
default: "main"
outputs:
version:
description: "The version that was built"
value: ${{ jobs.set-version.outputs.version }}
jobs:
set-version:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
version: ${{ steps.version.outputs.version }}
build_from: ${{ steps.version.outputs.build_from }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
- name: Set version
id: version
run: |
if [[ "${{inputs.version}}" != "" ]]; then
VERSION=${{inputs.version}}
else
BASE_VERSION=$(cat version.txt)
BRANCH_NAME="${{ github.ref_name }}"
if [[ "${BRANCH_NAME}" == "main" ]]; then
VERSION=${BASE_VERSION}-${{ github.run_number }}
elif [[ "${BRANCH_NAME}" =~ ^hotfix/.* ]]; then
# Special handling for hotfix branches
VERSION="${BASE_VERSION}-hotfix.${{ github.run_number }}"
else
# Normalize branch name
fixedBranchName=$(echo "${BRANCH_NAME}" | sed 's/origin\///; s/\//-/g; s/_/-/g; s/.*/\L&/')
if [ ${#fixedBranchName} -gt 21 ]; then
fixedBranchName="${fixedBranchName:0:20}"
# remove dash at the end
while [ "${fixedBranchName: -1}" = "-" ]; do
fixedBranchName="${fixedBranchName%?}"
done
fi
VERSION="${BASE_VERSION}-${fixedBranchName}-${{ github.run_number }}"
fi
fi
echo "version=${VERSION}" | tee -a "$GITHUB_OUTPUT"
echo "Version is ${VERSION}" >> "$GITHUB_STEP_SUMMARY"
if [ "${{inputs.ref}}" != "" ]; then
echo "build_from=${{inputs.ref}}" | tee -a "$GITHUB_OUTPUT"
elif [[ "${{inputs.version}}" == "" ]]; then
echo "build_from=${{github.ref_name}}" | tee -a "$GITHUB_OUTPUT"
else
echo "build_from=${{inputs.version}}" | tee -a "$GITHUB_OUTPUT"
fi
build-backend:
needs:
- set-version
secrets: inherit
uses: ./.github/workflows/build_and_push_docker.yaml
with:
image: "opik-backend"
version: ${{needs.set-version.outputs.version}}
build_from: ${{needs.set-version.outputs.build_from}}
build_comet_image: false
comet_build_args: ""
is_release: ${{ inputs.is_release || false }}
is_adhoc: ${{ inputs.is_adhoc || false }}
# push events have no `inputs` context, so force multi-arch there; otherwise honor the input (workflow_dispatch / workflow_call use the declared default: true)
build_arm64: ${{ github.event_name == 'push' || inputs.build_arm64 }}
build-sandbox-executor-python:
if: ${{ !inputs.is_adhoc }}
needs:
- set-version
secrets: inherit
uses: ./.github/workflows/build_and_push_docker.yaml
with:
image: "opik-sandbox-executor-python"
version: ${{needs.set-version.outputs.version}}
build_from: ${{needs.set-version.outputs.build_from}}
build_comet_image: false
comet_build_args: ""
is_release: ${{ inputs.is_release || false }}
is_adhoc: ${{ inputs.is_adhoc || false }}
build_arm64: ${{ github.event_name == 'push' || inputs.build_arm64 }}
build-python-backend:
if: always() && needs.set-version.result == 'success'
needs:
- set-version
- build-sandbox-executor-python
secrets: inherit
uses: ./.github/workflows/build_and_push_docker.yaml
with:
image: "opik-python-backend"
version: ${{needs.set-version.outputs.version}}
build_from: ${{needs.set-version.outputs.build_from}}
build_comet_image: false
comet_build_args: ""
is_release: ${{ inputs.is_release || false }}
is_adhoc: ${{ inputs.is_adhoc || false }}
build_arm64: ${{ github.event_name == 'push' || inputs.build_arm64 }}
# build-guardrails-backend:
# if: inputs.build_guardrails != false
# needs:
# - set-version
# secrets: inherit
# uses: ./.github/workflows/build_and_push_docker.yaml
# with:
# image: "opik-guardrails-backend"
# version: ${{needs.set-version.outputs.version}}
# build_from: ${{needs.set-version.outputs.build_from}}
# build_comet_image: false
# comet_build_args: ""
# is_release: ${{ inputs.is_release || false }}
build-frontend:
needs:
- set-version
secrets: inherit
uses: ./.github/workflows/build_and_push_docker.yaml
with:
image: "opik-frontend"
version: ${{needs.set-version.outputs.version}}
build_from: ${{needs.set-version.outputs.build_from}}
build_comet_image: ${{ !inputs.is_adhoc }}
comet_build_args: ${{ !inputs.is_adhoc && 'BUILD_MODE=comet' || '' }}
is_release: ${{ inputs.is_release || false }}
is_adhoc: ${{ inputs.is_adhoc || false }}
build_arm64: ${{ github.event_name == 'push' || inputs.build_arm64 }}
ai_spend_plugin_ref: ${{ inputs.ai_spend_plugin_ref || 'main' }}
create-git-tag:
if: |
always() &&
!inputs.is_release &&
needs.set-version.result == 'success' &&
needs.build-backend.result == 'success' &&
needs.build-frontend.result == 'success' &&
needs.build-python-backend.result == 'success'
needs:
- set-version
- build-backend
- build-frontend
- build-python-backend
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Create git tag
run: |
set -x
git config --local user.email "github-actions@comet.com"
git config --local user.name "github-actions"
git tag ${{needs.set-version.outputs.version}}
git push --no-verify origin refs/tags/${{needs.set-version.outputs.version}}
+92
View File
@@ -0,0 +1,92 @@
name: Build E2E Docker Images
on:
workflow_call:
inputs:
include_guardrails:
description: "Build and include guardrails-backend image"
required: false
default: false
type: boolean
outputs:
image_tag:
description: "The GHCR image tag used for this build"
value: ${{ jobs.build.outputs.image_tag }}
env:
DOCKER_REGISTRY: ghcr.io/comet-ml/opik
jobs:
build:
name: Build E2E Docker Images
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
packages: write
outputs:
image_tag: ${{ steps.tag.outputs.tag }}
steps:
- name: Set image tag
id: tag
run: |
SHA="${{ github.sha }}"
SHORT_SHA="${SHA:0:7}"
SUFFIX="${{ inputs.include_guardrails == true && '-guardrails' || '' }}"
if [ -n "${{ github.event.pull_request.number }}" ]; then
TAG="ci-e2e-pr${{ github.event.pull_request.number }}-${SHORT_SHA}${SUFFIX}"
else
TAG="ci-e2e-${SHORT_SHA}${SUFFIX}"
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Checkout
uses: actions/checkout@v6
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build E2E Docker images
env:
COMPOSE_BAKE: false
run: |
SERVICES="backend python-backend"
if [ "${{ inputs.include_guardrails }}" = "true" ]; then
SERVICES="$SERVICES guardrails-backend"
fi
# shellcheck disable=SC2086
docker compose -f deployment/docker-compose/docker-compose.yaml \
build $SERVICES
- name: Push images to GHCR
run: |
TAG="${{ steps.tag.outputs.tag }}"
IMAGES="opik-backend opik-python-backend"
if [ "${{ inputs.include_guardrails }}" = "true" ]; then
IMAGES="${IMAGES} opik-guardrails-backend"
fi
# Tag all images first (cheap, sequential)
for IMAGE in ${IMAGES}; do
docker tag "${{ env.DOCKER_REGISTRY }}/${IMAGE}:latest" \
"${{ env.DOCKER_REGISTRY }}/${IMAGE}:${TAG}"
done
# Push all images in parallel
PIDS=()
for IMAGE in ${IMAGES}; do
docker push "${{ env.DOCKER_REGISTRY }}/${IMAGE}:${TAG}" &
PIDS+=($!)
done
# Wait for all pushes, fail if any fails
FAILED=0
for PID in "${PIDS[@]}"; do
wait "${PID}" || FAILED=1
done
exit "${FAILED}"
+112
View File
@@ -0,0 +1,112 @@
name: Cleanup E2E Docker Images
on:
pull_request:
types: [closed]
schedule:
- cron: "0 3 * * 6" # Every Saturday at 03:00 UTC
workflow_dispatch:
inputs:
max_age_days:
description: "Delete CI tags older than this many days (0 = delete all)"
required: false
default: 7
type: number
permissions:
contents: read
packages: write
env:
IMAGES: >-
opik-backend
opik-python-backend
opik-guardrails-backend
jobs:
cleanup:
name: Cleanup CI images
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Delete CI images
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EVENT_NAME: ${{ github.event_name }}
PR_NUMBER: ${{ github.event.pull_request.number }}
MAX_AGE_DAYS: ${{ github.event.inputs.max_age_days || '7' }}
run: |
DELETED=0
SKIPPED=0
FAILED=0
# Build the jq filter and age cutoff based on trigger type
if [ "$EVENT_NAME" = "pull_request" ]; then
echo "Mode: PR close — deleting all ci-e2e-pr${PR_NUMBER}-* images"
JQ_FILTER=".[] | select(.metadata.container.tags | any(startswith(\"ci-e2e-pr${PR_NUMBER}-\"))) | {id: .id, tags: .metadata.container.tags, created_at: .created_at}"
CUTOFF=""
else
echo "Mode: stale sweep — deleting ci-e2e-* images older than ${MAX_AGE_DAYS} days"
JQ_FILTER='.[] | select(.metadata.container.tags | any(startswith("ci-e2e-"))) | {id: .id, tags: .metadata.container.tags, created_at: .created_at}'
CUTOFF=$(date -u -d "${MAX_AGE_DAYS} days ago" +%s 2>/dev/null \
|| date -u -v-"${MAX_AGE_DAYS}"d +%s)
fi
for IMAGE in ${{ env.IMAGES }}; do
echo "=== Scanning ${IMAGE} ==="
if ! VERSIONS=$(gh api \
"/orgs/comet-ml/packages/container/opik%2F${IMAGE}/versions" \
--paginate \
--jq "$JQ_FILTER" 2>&1); then
echo " Failed to list versions: ${VERSIONS}"
continue
fi
if [ -z "$VERSIONS" ]; then
echo " No matching versions found"
continue
fi
COUNT=$(echo "$VERSIONS" | wc -l | tr -d ' ')
echo " Found ${COUNT} matching versions"
while read -r VERSION; do
[ -z "$VERSION" ] && continue
VERSION_ID=$(echo "$VERSION" | jq -r '.id')
CI_TAG=$(echo "$VERSION" | jq -r '.tags[] | select(startswith("ci-e2e-"))' | head -1)
# For stale sweep, check age; for PR close, delete unconditionally
if [ -n "$CUTOFF" ]; then
CREATED_AT=$(echo "$VERSION" | jq -r '.created_at')
CREATED_TS=$(date -u -d "$CREATED_AT" +%s 2>/dev/null \
|| date -u -j -f "%Y-%m-%dT%H:%M:%S" "${CREATED_AT%%.*}" +%s 2>/dev/null \
|| echo "")
if [ -z "$CREATED_TS" ]; then
echo " Skipping ${IMAGE}:${CI_TAG} (could not parse created_at: ${CREATED_AT})"
SKIPPED=$((SKIPPED + 1))
continue
fi
if [ "$CREATED_TS" -ge "$CUTOFF" ]; then
SKIPPED=$((SKIPPED + 1))
continue
fi
echo " Deleting ${IMAGE}:${CI_TAG} (older than ${MAX_AGE_DAYS} days)"
else
echo " Deleting ${IMAGE}:${CI_TAG}"
fi
if gh api --method DELETE \
"/orgs/comet-ml/packages/container/opik%2F${IMAGE}/versions/${VERSION_ID}"; then
DELETED=$((DELETED + 1))
else
FAILED=$((FAILED + 1))
fi
done <<< "$VERSIONS"
done
echo ""
echo "=== Summary ==="
echo "Deleted: ${DELETED}"
echo "Skipped: ${SKIPPED}"
echo "Failed: ${FAILED}"
@@ -0,0 +1,134 @@
name: "ClickHouse Migration Cluster Check"
run-name: "ClickHouse Migration Cluster Check on ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
on:
pull_request:
paths:
- "apps/opik-backend/src/main/resources/liquibase/db-app-analytics/migrations/**/*.sql"
push:
branches:
- "main"
paths:
- "apps/opik-backend/src/main/resources/liquibase/db-app-analytics/migrations/**/*.sql"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
validate-clickhouse-migrations:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Get changed ClickHouse migration files
id: changed-files
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "🔍 Detecting ClickHouse migration files that were added or modified..."
if [ "${{ github.event_name }}" = "pull_request" ]; then
# For PRs, use GitHub's built-in files API
gh api repos/${{ github.repository }}/pulls/${{ github.event.number }}/files \
--jq '.[] | select(.status == "added" or .status == "modified") | select(.filename | test("^apps/opik-backend/src/main/resources/liquibase/db-app-analytics/migrations/.*\\.sql$")) | .filename' > changed_files.txt
CHANGED_FILES=$(cat changed_files.txt || echo "")
rm -f changed_files.txt
else
# For pushes to main, use GitHub API to get commit files
# This works reliably regardless of fetch-depth
gh api repos/${{ github.repository }}/commits/${{ github.sha }} \
--jq '.files[] | select(.status == "added" or .status == "modified") | select(.filename | test("^apps/opik-backend/src/main/resources/liquibase/db-app-analytics/migrations/.*\\.sql$")) | .filename' > changed_files.txt
CHANGED_FILES=$(cat changed_files.txt || echo "")
rm -f changed_files.txt
fi
if [ -n "$CHANGED_FILES" ]; then
echo "📄 Found ClickHouse migration files to validate:"
# shellcheck disable=SC2086 # intentional word-split on newlines in CHANGED_FILES
printf '%s\n' $CHANGED_FILES | while read -r file; do
[ -n "$file" ] && echo " - $file"
done
# Save the files to environment variable for next step
echo "MIGRATION_FILES<<EOF" >> "$GITHUB_ENV"
# shellcheck disable=SC2086 # intentional word-split on newlines in CHANGED_FILES
printf '%s\n' $CHANGED_FILES | while read -r file; do
[ -n "$file" ] && echo "$file" >> "$GITHUB_ENV"
done
echo "EOF" >> "$GITHUB_ENV"
echo "HAS_MIGRATION_FILES=true" >> "$GITHUB_ENV"
else
echo "📋 No ClickHouse migration files were added or modified."
echo "✅ No validation needed."
echo "HAS_MIGRATION_FILES=false" >> "$GITHUB_ENV"
fi
- name: Validate ClickHouse migrations have ON CLUSTER clause
if: env.HAS_MIGRATION_FILES == 'true'
run: |
echo "🔍 Validating ClickHouse migrations for proper ON CLUSTER clause usage..."
echo "📖 Reference: https://clickhouse.com/docs/sql-reference/distributed-ddl"
echo
# Run the validation script with the migration files
# Use xargs with -0 flag and tr to handle newline-separated list safely
# This approach correctly handles filenames with spaces across different platforms
if echo "$MIGRATION_FILES" | tr '\n' '\0' | xargs -0 ./scripts/check_clickhouse_migrations_cluster.sh; then
echo "✅ All modified ClickHouse migrations are properly configured!"
echo "🎉 DDL operations will be executed distributively across the cluster."
else
echo "❌ ClickHouse migration validation failed!"
echo
echo "💡 What this means:"
echo " - DDL operations without ON CLUSTER clause will only execute on a single node"
echo " - This can cause inconsistencies in distributed ClickHouse deployments"
echo " - All CREATE, DROP, ALTER, RENAME, and EXCHANGE statements must include ON CLUSTER '{cluster}'"
echo
echo "🔧 How to fix:"
echo " 1. Add 'ON CLUSTER '\"'\"'{cluster}'\"'\"'' to all DDL statements in your migration files"
echo " 2. Example: CREATE TABLE my_table ON CLUSTER '{cluster}' (...)"
echo " 3. Reference: https://clickhouse.com/docs/sql-reference/distributed-ddl"
echo
exit 1
fi
- name: Summary for no migration files
if: env.HAS_MIGRATION_FILES == 'false'
run: |
echo "=========================================="
echo "✅ No ClickHouse Migration Files Changed"
echo "=========================================="
echo "📋 No ClickHouse migration files were added or modified in this change."
echo "✅ No validation required."
echo
echo "💡 Note: This check only validates NEW or MODIFIED migration files."
echo " Existing migration files are not checked as they cannot be changed."
- name: Summary for validated files
if: env.HAS_MIGRATION_FILES == 'true' && success()
run: |
echo "=========================================="
echo "✅ ClickHouse Migration Validation PASSED"
echo "=========================================="
echo "✅ All modified DDL operations include proper ON CLUSTER clause"
echo "✅ Migrations are ready for distributed ClickHouse deployment"
echo "✅ No cluster inconsistencies will occur"
echo
echo "🎯 This check ensures that:"
echo " • CREATE statements will create tables/indexes on all cluster nodes"
echo " • DROP statements will remove objects from all cluster nodes"
echo " • ALTER statements will modify schema on all cluster nodes"
echo " • RENAME statements will rename objects on all cluster nodes"
echo
echo "📚 For more information about distributed DDL:"
echo " https://clickhouse.com/docs/sql-reference/distributed-ddl"
+337
View File
@@ -0,0 +1,337 @@
name: '🐙 Code Quality'
run-name: "🐙 Code Quality ${{ github.ref_name }} by @${{ github.actor }}"
# Lint + format + typecheck across all languages, driven by the root
# .pre-commit-config.yaml (the source of truth for which checks run on which
# paths). Changed-files-only, mirroring local commits.
#
# Shape: one CI job PER LINTER that actually has work.
# detect — replays pre-commit's path matching over the diff and emits one
# matrix leg per hook with ≥1 matching file (no skip-spam legs).
# lint — fan-out matrix, one job per linter, fail-fast: false, so each
# linter is reported, retried, and made visible independently.
# summary — aggregates every leg's timing into one sticky PR comment and
# gates the workflow red if any leg failed.
#
# PRs lint the whole PR diff (base..head); pushes to main lint the push range.
# A full-repo audit is available on demand via workflow_dispatch.
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
inputs:
all_files:
description: "Run all hooks against the whole repo (full audit)"
type: boolean
default: true
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
# Needed by the summary job to post/update the sticky timing comment on PRs.
# On fork PRs the token is read-only regardless, so that step is guarded.
pull-requests: write
jobs:
# ---------------------------------------------------------------------------
# Resolve the diff range and emit one matrix leg per hook that has matching
# changed files. The matrix is built from CHANGED hooks only, so a linter that
# matches nothing never spawns a (skipped) job.
# ---------------------------------------------------------------------------
detect:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
legs: ${{ steps.detect.outputs.legs }}
has_legs: ${{ steps.detect.outputs.has_legs }}
legs_count: ${{ steps.detect.outputs.legs_count }}
skipped: ${{ steps.detect.outputs.skipped }}
from: ${{ steps.range.outputs.from }}
to: ${{ steps.range.outputs.to }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
# Diff refs (base/head, push before/after) must be reachable.
fetch-depth: 0
- name: Resolve diff range
id: range
env:
EVENT: ${{ github.event_name }}
PR_BASE: ${{ github.event.pull_request.base.sha }}
PR_HEAD: ${{ github.event.pull_request.head.sha }}
PUSH_BEFORE: ${{ github.event.before }}
PUSH_AFTER: ${{ github.sha }}
run: |
case "$EVENT" in
pull_request) FROM="$PR_BASE"; TO="$PR_HEAD" ;;
push)
FROM="$PUSH_BEFORE"; TO="$PUSH_AFTER"
if [ "$FROM" = "0000000000000000000000000000000000000000" ]; then FROM="${TO}^"; fi ;;
*) FROM=""; TO="" ;; # workflow_dispatch: full-repo audit
esac
echo "from=$FROM" >> "$GITHUB_OUTPUT"
echo "to=$TO" >> "$GITHUB_OUTPUT"
- name: Detect hooks with work
id: detect
env:
FROM: ${{ steps.range.outputs.from }}
TO: ${{ steps.range.outputs.to }}
run: |
set -o pipefail
if [ -n "$FROM" ]; then
# ACMR: added/copied/modified/renamed only. Exclude deletions —
# pre-commit lints files present in the worktree, so a deleted path
# would spawn a leg that finds "no files to check" and Skips.
mapfile -t changed < <(git diff --name-only --diff-filter=ACMR "$FROM..$TO")
else
# workflow_dispatch full audit: every tracked file is "changed".
mapfile -t changed < <(git ls-files)
fi
out="$(printf '%s\n' "${changed[@]}" \
| python3 scripts/precommit-detect-hooks.py .pre-commit-config.yaml)"
# Full {legs, skipped} JSON drives the matrix (.legs) and the summary
# (.skipped). legs_count lets the summary reconcile rendered rows
# against the jobs that should have produced timing.
echo "legs=$out" >> "$GITHUB_OUTPUT"
echo "skipped=$(echo "$out" | python3 -c 'import json,sys; print(json.dumps(json.load(sys.stdin)["skipped"]))')" >> "$GITHUB_OUTPUT"
count="$(echo "$out" | python3 -c 'import json,sys; print(len(json.load(sys.stdin)["legs"]))')"
echo "legs_count=$count" >> "$GITHUB_OUTPUT"
if [ "$count" -gt 0 ]; then echo "has_legs=true" >> "$GITHUB_OUTPUT"; else echo "has_legs=false" >> "$GITHUB_OUTPUT"; fi
echo "Detected $count hook(s) with work."
# ---------------------------------------------------------------------------
# One job per linter. Each leg provisions only the toolchain it needs, runs
# its single hook over its matched files, and uploads a timing fragment for
# the summary. fail-fast: false so one red linter doesn't cancel the others
# (independent reporting + single-leg re-run).
# ---------------------------------------------------------------------------
lint:
needs: detect
if: needs.detect.outputs.has_legs == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
leg: ${{ fromJSON(needs.detect.outputs.legs).legs }}
name: "lint (${{ matrix.leg.name }})"
steps:
- name: Checkout
uses: actions/checkout@v6
with:
# Shallow: the leg lints explicit files (paths come from detect's
# matrix value, not a git diff here), so it needs the worktree content
# at HEAD, not history. Avoids a full clone on every one of N legs.
fetch-depth: 1
- name: Set up JDK 25
if: matrix.leg.toolchain == 'java'
uses: actions/setup-java@v4
with:
java-version: "25"
distribution: "corretto"
cache: maven
- name: Set up Node.js
if: matrix.leg.toolchain == 'node-fe' || matrix.leg.toolchain == 'node-ts'
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install frontend deps
if: matrix.leg.toolchain == 'node-fe'
run: npm ci
working-directory: apps/opik-frontend
- name: Install TypeScript SDK deps
if: matrix.leg.toolchain == 'node-ts'
run: npm ci
working-directory: sdks/typescript
- name: Set up uv
uses: astral-sh/setup-uv@v5
- name: Cache pre-commit environments
uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
# Run exactly this leg's hook over its matched files. Hook ids aren't
# unique across scopes, so `--files` narrows a shared id (e.g. ruff) to
# the one scope that matched; sibling hooks report "no files to check".
# Capture the verbose log and exit code, then filter the log to this
# hook's own result lines so the summary table shows one row per linter.
- name: Run hook
id: hook
env:
HOOK_ID: ${{ matrix.leg.id }}
HOOK_FILES: ${{ matrix.leg.files }}
run: |
set -o pipefail
set +e
# shellcheck disable=SC2086 # HOOK_FILES is an intentional word-split list
uvx pre-commit run "$HOOK_ID" --files $HOOK_FILES --show-diff-on-failure --verbose 2>&1 \
| tee /tmp/leg-raw.log
echo "exit_code=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"
set -e
./scripts/precommit-filter-leg-log.sh "$HOOK_ID" /tmp/leg-raw.log > /tmp/leg.log
cat /tmp/leg.log
- name: Sanitize artifact name
id: artifact
env:
HOOK_NAME: ${{ matrix.leg.name }}
run: |
# Artifact names can't contain emojis/spaces/slashes; derive a safe,
# collision-free one. The summary downloads with merge-multiple (all
# fragments flattened into one dir), so the FILE inside must be unique
# too — name it the same as the artifact, not a shared "leg.log".
safe="$(echo "$HOOK_NAME" | tr -cd '[:alnum:]_-' | head -c 80)"
[ -n "$safe" ] || safe="hook"
frag="timing-${safe}-${{ strategy.job-index }}"
echo "name=$frag" >> "$GITHUB_OUTPUT"
cp /tmp/leg.log "/tmp/${frag}.log"
- name: Upload timing fragment
if: always()
uses: actions/upload-artifact@v4
with:
name: ${{ steps.artifact.outputs.name }}
path: /tmp/${{ steps.artifact.outputs.name }}.log
if-no-files-found: ignore
retention-days: 1
- name: Fail leg if hook failed
if: steps.hook.outputs.exit_code != '0'
run: |
echo "::error::${{ matrix.leg.name }} failed (exit ${{ steps.hook.outputs.exit_code }})."
exit 1
# ---------------------------------------------------------------------------
# Aggregate every leg's timing fragment into ONE slowest-first table, post it
# as a sticky PR comment (overwritten each run), and gate the workflow red if
# any leg failed. Runs even when legs fail (if: always) so the comment + the
# remediation always land. Also runs when detect found no work (posts a clean
# "nothing to lint" comment and passes).
# ---------------------------------------------------------------------------
summary:
needs: [detect, lint]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v6
with:
# Only needs the render scripts + shared description TSV, not history.
fetch-depth: 1
- name: Download timing fragments
if: needs.detect.outputs.has_legs == 'true'
uses: actions/download-artifact@v4
with:
path: /tmp/fragments
pattern: timing-*
merge-multiple: true
- name: Render timing table
env:
HAS_LEGS: ${{ needs.detect.outputs.has_legs }}
LEGS_COUNT: ${{ needs.detect.outputs.legs_count }}
SKIPPED_JSON: ${{ needs.detect.outputs.skipped }}
LINT_RESULT: ${{ needs.lint.result }}
run: |
# Build the running-hooks table first so we can reconcile its row
# count against the number of jobs detect spawned: every running leg
# must contribute exactly one row. A shortfall means a leg ran but its
# fragment was empty (e.g. detect over-emitted a leg that Skipped at
# runtime) — surface it instead of silently under-reporting.
ran_rows=0
if [ "$HAS_LEGS" = "true" ]; then
cat /tmp/fragments/*.log > /tmp/precommit-output.log 2>/dev/null || true
if [ -s /tmp/precommit-output.log ]; then
./scripts/precommit-timing-table.sh /tmp/precommit-output.log > /tmp/timing-rows.md
# Data rows = "| "-prefixed lines minus the header and Total rows.
# (The "|---" separator starts with "|-", so it isn't counted.)
ran_rows="$(grep -c '^| ' /tmp/timing-rows.md || true)"
ran_rows=$(( ran_rows - 2 ))
[ "$ran_rows" -lt 0 ] && ran_rows=0
fi
fi
{
# Hidden marker lets find-comment locate this PR's comment so it's
# updated in place each run (no per-commit spam).
echo "<!-- precommit-timing -->"
echo "### ⏱️ pre-commit per-hook timing"
echo ""
if [ "$HAS_LEGS" != "true" ]; then
echo "_No linted files changed — nothing to run._"
elif [ -s /tmp/timing-rows.md ]; then
cat /tmp/timing-rows.md
else
echo "_No pre-commit output captured._"
fi
# Reconcile: rendered rows must equal the jobs detect spawned.
if [ "$HAS_LEGS" = "true" ] && [ "${ran_rows}" -ne "${LEGS_COUNT:-0}" ]; then
echo ""
echo "> [!WARNING]"
echo "> Timing shows **${ran_rows}** of **${LEGS_COUNT}** lint jobs — $(( LEGS_COUNT - ran_rows )) produced no timing (ran but matched no files at runtime). detect over-emitted a leg; see \`scripts/precommit-detect-hooks.py\` (\`TYPED_IDS\`)."
fi
# Skipped checks — hooks with no matching files (no job ran).
./scripts/precommit-skipped-table.sh "$SKIPPED_JSON"
# Remediation — only when a leg failed, so green runs stay clean.
if [ "$LINT_RESULT" = "failure" ]; then
echo ""
echo "> [!WARNING]"
echo "> **Lint failed.** Fix locally, then push again:"
echo "> \`\`\`bash"
echo "> pip install pre-commit # or: brew install pre-commit"
echo "> make hooks # install the git hook (once per clone)"
echo "> make precommit # run the same checks on your changed files"
echo "> \`\`\`"
echo "> Formatters fix files in place — review the changes, \`git add\`, and commit."
echo "> Or apply the diff shown in the failed CI leg directly."
fi
} > /tmp/precommit-table.md
cat /tmp/precommit-table.md >> "$GITHUB_STEP_SUMMARY"
- name: Find prior timing comment
id: find_comment
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
uses: peter-evans/find-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: github-actions[bot]
body-includes: "<!-- precommit-timing -->"
- name: Post or update timing comment
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find_comment.outputs.comment-id }}
edit-mode: replace
body-path: /tmp/precommit-table.md
# Gate: the workflow is red iff any lint leg failed. (detect/summary infra
# failures surface on their own.) Fork PRs get no sticky comment, so the
# remediation also lives in this step log — the one place every contributor
# can see.
- name: Gate on lint result
if: needs.lint.result == 'failure'
run: |
echo "::error::One or more linters failed. Fix locally:"
echo " pip install pre-commit # or: brew install pre-commit"
echo " make hooks # install the git hook (once per clone)"
echo " make precommit # run the same checks on your changed files"
echo "Formatters fix files in place — review, git add, and commit. Or apply the diff shown in the failed leg."
exit 1
@@ -0,0 +1,51 @@
name: Deploy Opik Python SDK docs
run-name: Deploy Python SDK docs ${{ github.ref_name }} to S3 ${{ inputs.environment }} by @${{ github.actor }}
on:
workflow_dispatch:
inputs:
environment:
type: choice
description: Choose environment
options:
- staging
- production
- development
jobs:
upload:
runs-on: ubuntu-latest
timeout-minutes: 15
environment: ${{ inputs.environment }}
permissions:
id-token: write # required for OIDC
contents: read
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ vars.AWS_REGION }}
steps:
- name: Verify AWS credentials
run: aws sts get-caller-identity
- name: checkout
uses: actions/checkout@v6
with:
show-progress: false
- name: Setup uv
uses: astral-sh/setup-uv@v8.2.0
- name: Build Python SDK docs
run: |
echo "Build sdk documentation"
cd apps/opik-documentation/python-sdk-docs
uv venv .venv
uv pip install -r requirements.txt
uv pip install ../../../sdks/python
uv run make build
- name: Upload sdk docs to site.comet.com (${{ inputs.environment }})
run: |
aws s3 sync apps/opik-documentation/python-sdk-docs/build/html \
s3://site.comet.com/${{ inputs.environment }}/docs-opik/python-sdk-reference \
--follow-symlinks --no-progress --delete --exclude '.git*'
@@ -0,0 +1,193 @@
name: Docker Vulnerability Scan
on:
schedule:
# Run Monday at midnight UTC
- cron: '0 0 * * 1'
workflow_dispatch:
inputs:
image-tag:
description: 'Image tag to scan'
required: true
default: main
type: string
permissions:
contents: read
env:
DOCKER_REGISTRY: "ghcr.io/comet-ml/opik"
jobs:
scan-docker-images:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
image:
- opik-backend
- opik-frontend
- opik-frontend-comet
- opik-python-backend
- opik-sandbox-executor-python
name: Scan ${{ matrix.image }}
steps:
- name: Pull Docker image
id: pull
run: |
IMAGE_URL="${{ env.DOCKER_REGISTRY }}/${{ matrix.image }}:${{ inputs.image-tag || 'main' }}"
echo "Pulling Docker image: ${IMAGE_URL}"
docker pull ${IMAGE_URL}
BASENAME=$(basename ${IMAGE_URL} | cut -d: -f1)
REPORT_NAME="trivy_${BASENAME}.txt"
echo "image-url=${IMAGE_URL}" >> "$GITHUB_OUTPUT"
echo "report-name=${REPORT_NAME}" >> "$GITHUB_OUTPUT"
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@v0.35.0
env:
TRIVY_DISABLE_VEX_NOTICE: true
with:
image-ref: '${{ steps.pull.outputs.image-url }}'
scan-type: 'image'
format: 'table'
ignore-unfixed: 'false'
output: '${{ steps.pull.outputs.report-name }}'
scanners: vuln
severity: 'CRITICAL,HIGH'
hide-progress: true
timeout: 10m0s
- name: Set outputs
id: set-outputs
run: |
REPORT_NAME="${{ steps.pull.outputs.report-name }}"
echo "report-file=${REPORT_NAME}" >> "$GITHUB_OUTPUT"
if [ -f "${REPORT_NAME}" ]; then
echo "Report generated: ${REPORT_NAME}"
ls -lh "${REPORT_NAME}"
else
echo "Warning: Report file not found at ${REPORT_NAME}"
fi
- name: Write to job summary
run: |
{
echo "## Trivy Vulnerability Scan: \`${{ steps.pull.outputs.image-url }}\`"
echo ""
echo "<details><summary>Click to expand report</summary>"
echo ""
echo '```'
cat "${{ steps.pull.outputs.report-name }}" 2>/dev/null || echo "Report not found"
echo '```'
echo ""
echo "</details>"
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload scan results as artifact
uses: actions/upload-artifact@v7
with:
name: trivy-scan-${{ matrix.image }}
path: ${{ steps.pull.outputs.report-name }}
retention-days: 30
if-no-files-found: warn
- name: Checkout scripts
uses: actions/checkout@v6
with:
sparse-checkout: scripts/analyze_trivy_report.sh
sparse-checkout-cone-mode: false
path: repo
- name: Analyze vulnerabilities
id: analyze
run: |
./repo/scripts/analyze_trivy_report.sh \
"${{ steps.pull.outputs.report-name }}" \
"${{ steps.pull.outputs.image-url }}" \
--workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--artifact-name "trivy-scan-${{ matrix.image }}"
- name: Format base image Slack message
if: steps.analyze.outputs.has-base-image-vulns == 'true'
id: format-base
env:
MESSAGE: ${{ steps.analyze.outputs.base-image-message }}
MENTION: ${{ secrets.SLACK_MENTION_USERS }}
run: |
python3 << 'PYTHON_SCRIPT'
import re, os
input_message = os.environ.get('MESSAGE', '').strip()
mention = os.environ.get('MENTION', '').strip()
result = re.sub(r"'(.*?)'", r"`\1`", input_message)
if mention:
# Ensure each user ID is wrapped in <@...> for Slack mention syntax
parts = mention.split()
wrapped = ' '.join(
uid if uid.startswith('<@') else f'<@{uid}>'
for uid in parts
)
result = wrapped + '\n\n' + result
with open(os.environ.get('GITHUB_OUTPUT', '/dev/null'), 'a') as f:
f.write('message<<EOF\n')
f.write(result)
f.write('\nEOF\n')
PYTHON_SCRIPT
- name: Format app Slack message
if: steps.analyze.outputs.has-app-vulns == 'true'
id: format-app
env:
MESSAGE: ${{ steps.analyze.outputs.app-message }}
MENTION: ${{ secrets.SLACK_MENTION_USERS }}
run: |
python3 << 'PYTHON_SCRIPT'
import re, os
input_message = os.environ.get('MESSAGE', '').strip()
mention = os.environ.get('MENTION', '').strip()
result = re.sub(r"'(.*?)'", r"`\1`", input_message)
if mention:
# Ensure each user ID is wrapped in <@...> for Slack mention syntax
parts = mention.split()
wrapped = ' '.join(
uid if uid.startswith('<@') else f'<@{uid}>'
for uid in parts
)
result = wrapped + '\n\n' + result
with open(os.environ.get('GITHUB_OUTPUT', '/dev/null'), 'a') as f:
f.write('message<<EOF\n')
f.write(result)
f.write('\nEOF\n')
PYTHON_SCRIPT
- name: Notify Slack - base image vulnerabilities
if: steps.analyze.outputs.has-base-image-vulns == 'true'
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_VULNERABILITY_WEBHOOK_URL }}
SLACK_COLOR: warning
SLACK_USERNAME: 'GitHub Actions'
SLACK_ICON: 'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png'
SLACK_LINK_NAMES: true
MSG_MINIMAL: false
ENABLE_ESCAPES: true
SLACK_TITLE: ${{ steps.analyze.outputs.base-image-title }}
SLACK_MESSAGE: |
*Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}>
${{ steps.format-base.outputs.message }}
- name: Notify Slack - application vulnerabilities
if: steps.analyze.outputs.has-app-vulns == 'true'
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_VULNERABILITY_WEBHOOK_URL }}
SLACK_COLOR: danger
SLACK_USERNAME: 'GitHub Actions'
SLACK_ICON: 'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png'
SLACK_LINK_NAMES: true
MSG_MINIMAL: false
ENABLE_ESCAPES: true
SLACK_TITLE: ${{ steps.analyze.outputs.app-title }}
SLACK_MESSAGE: |
*Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}>
${{ steps.format-app.outputs.message }}
@@ -0,0 +1,74 @@
name: Docs - Test cookbooks
on:
workflow_dispatch:
inputs:
install_opik:
description: 'Enable opik installation from source files'
required: false
default: 'false'
type: choice
options:
- 'false'
- 'true'
jobs:
test-notebooks:
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest]
python-version: ["3.12"]
notebooks:
- apps/opik-documentation/documentation/docs/cookbook/quickstart_notebook.ipynb
- apps/opik-documentation/documentation/docs/cookbook/evaluate_hallucination_metric.ipynb
- apps/opik-documentation/documentation/docs/cookbook/evaluate_moderation_metric.ipynb
- apps/opik-documentation/documentation/docs/cookbook/langchain.ipynb
- apps/opik-documentation/documentation/docs/cookbook/langgraph.ipynb
- apps/opik-documentation/documentation/docs/cookbook/llama-index.ipynb
- apps/opik-documentation/documentation/docs/cookbook/openai.ipynb
- apps/opik-documentation/documentation/docs/cookbook/litellm.ipynb
- apps/opik-documentation/documentation/docs/cookbook/ragas.ipynb
- apps/opik-documentation/documentation/docs/cookbook/dspy.ipynb
- apps/opik-documentation/documentation/docs/cookbook/crewai.ipynb
- apps/opik-documentation/documentation/docs/cookbook/instructor.ipynb
env:
NOTEBOOK_TO_TEST: ${{ matrix.notebooks }}
OPIK_SENTRY_ENABLE: False
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Get pip cache dir
id: pip-cache
run: |
echo "dir=$(pip cache dir)" >> "$GITHUB_OUTPUT"
- name: pip cache
uses: actions/cache@v4
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: ${{ matrix.os }}-${{ matrix.python-version }}-pip
restore-keys: |
${{ matrix.os }}-${{ matrix.python-version }}-pip
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -U ipython nbconvert
- name: Install opik from source files (optional)
if: ${{ github.event_name == 'workflow_dispatch' && inputs.install_opik == 'true' }}
run: pip install sdks/python
- name: Prepare env variables
run: |
directory=$(dirname -- "${NOTEBOOK_TO_TEST}")
notebook=$(basename -- "${NOTEBOOK_TO_TEST}")
echo "TEST_DIRECTORY=${directory}" >> "$GITHUB_ENV"
echo "TEST_NOTEBOOK=${notebook}" >> "$GITHUB_ENV"
- name: Test notebook
run: |
cd "$TEST_DIRECTORY" || exit
python -X faulthandler "$(which ipython)" "$TEST_NOTEBOOK"
env:
OPENAI_API_KEY: ${{ secrets.DOCS_OPENAI_API_KEY }}
OPIK_API_KEY: ${{ secrets.COMET_API_KEY }}
OPIK_WORKSPACE: ${{ secrets.COMET_WORKSPACE }}
@@ -0,0 +1,33 @@
name: Docs - Publish
on:
workflow_dispatch:
push:
branches:
- main
paths:
- 'apps/opik-documentation/documentation/**'
jobs:
run:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # Fetch all history for git diff
- name: Install Fern
working-directory: apps/opik-documentation/documentation
run: npm install
- name: Publish Docs
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
SEGMENT_WRITE_KEY: ${{ secrets.SEGMENT_WRITE_KEY }}
GTM_CONTAINER_ID: ${{ secrets.GTM_CONTAINER_ID }}
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
POSTHOG_API_HOST: ${{ secrets.POSTHOG_API_HOST }}
run: |
cd apps/opik-documentation/documentation
npx fern generate --docs
@@ -0,0 +1,37 @@
name: Docs - Compress Images
on:
workflow_dispatch:
pull_request:
# Run Image Actions when JPG, JPEG, PNG or WebP files are added or changed.
# See https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#onpushpull_requestpaths for reference.
paths:
[
"apps/opik-documentation/documentation/fern/img/**.jpg",
"apps/opik-documentation/documentation/fern/img/**.jpeg",
"apps/opik-documentation/documentation/fern/img/**.png",
"apps/opik-documentation/documentation/fern/img/**.webp",
]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build:
# Only run on non-draft PRs within the same repository.
if:
github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.draft
== false
name: calibreapp/image-actions
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- name: Compress Images
uses: calibreapp/image-actions@1.1.0
with:
# The `GITHUB_TOKEN` is automatically generated by GitHub and scoped only to the repository that is currently running the action. By default, the action cant update Pull Requests initiated from forked repositories.
# See https://docs.github.com/en/actions/reference/authentication-in-a-workflow and https://help.github.com/en/articles/virtual-environments-for-github-actions#token-permissions
githubToken: ${{ secrets.GITHUB_TOKEN }}
ignorePaths: "apps/opik-frontend/**,apps/opik-backend/**,apps/opik-python-backend/**"
@@ -0,0 +1,147 @@
name: Docs - Preview link
on:
pull_request:
paths:
- 'apps/opik-documentation/documentation/**'
- '.github/workflows/documentation_preview_link.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
run:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions: write-all
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Fern
working-directory: apps/opik-documentation/documentation
run: npm install
- name: Generate preview URL
id: generate-docs
working-directory: apps/opik-documentation/documentation
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
run: |
OUTPUT=$(npx fern generate --docs --preview 2>&1) || true
echo "$OUTPUT"
URL=$(echo "$OUTPUT" | grep -oP 'Published docs to \K.*(?= \()')
echo "Preview URL: $URL"
echo "URL=$URL" >> "$GITHUB_OUTPUT"
echo "🌿 Preview your docs: $URL" > preview_url.txt
- name: Check for broken links
id: check-broken-links
working-directory: apps/opik-documentation/documentation
run: |
echo -e "\n\n" >> preview_url.txt
# --concurrency 500: the preview CDN serves bursts without rate-limiting;
# the recurse otherwise brushes the job's 15-minute timeout.
npx linkinator ${{ steps.generate-docs.outputs.URL }} \
--recurse \
--concurrency 500 \
--skip "search\/v2\/key" \
--skip "https://ai.google.dev/gemini-api" \
--skip "https://ai.google.dev/aistudio" \
--skip "chat.comet.com" \
--skip "http://localhost" \
--skip "http://localhost:5173" \
--skip "googletagmanager.com" \
--skip "insights/script.js" \
--format json > linkinator_results.json || true
PREVIEW_ORIGIN=$(echo "${{ steps.generate-docs.outputs.URL }}" | grep -oE '^https?://[^/]+')
# linkinator exits non-zero both when it finds broken links and when it
# crashes mid-crawl (e.g. unhandled ECONNRESET), so trust the output, not
# the exit code: invalid/missing JSON means the crawl didn't complete.
if ! jq -e 'type == "object" and (.links | type == "array")' linkinator_results.json > /dev/null 2>&1; then
{
echo "⚠️ The link check did not complete (likely a crawl crash or timeout). Please re-run this check."
echo ""
echo "---"
echo "📌 Results for commit ${{ github.sha }}"
} >> preview_url.txt
echo "true" > check_failed.flag
exit 0
fi
# `↳ on page` strips the ephemeral preview host, leaving a stable,
# greppable doc path. Append one report entry for a status-0 url whose
# recheck label is computed by the caller.
report_timeout() {
local url=$1 label=$2 parent
parent=$(jq -r --arg u "$url" --arg origin "$PREVIEW_ORIGIN" \
'(first(.links[] | select(.url == $u)).parent // "") | sub("^" + $origin; "")' linkinator_results.json)
BROKEN_LINKS=$(printf '%s\n❌ Broken link: %s (%s)\n ↳ on page: %s' "$BROKEN_LINKS" "$url" "$label" "$parent")
}
# Real HTTP errors (excluding 403s), deduped by url — one broken sidebar
# link otherwise multiplies into hundreds of lines and overflows GitHub's
# 65,536-char comment limit.
BROKEN_LINKS=$(jq -r --arg origin "$PREVIEW_ORIGIN" '
[.links[] | select(.state != "OK" and .status != 403 and .status != 0)]
| group_by(.url) | map(.[0]) | .[]
| "❌ Broken link: \(.url) (\(.status))\n ↳ on page: \((.parent // "") | sub("^" + $origin; ""))"
' linkinator_results.json)
# Status 0 is "crawl gave up", not an HTTP error — a false positive for
# healthy-but-slow pages. Re-check each (only on the preview origin, so a
# PR-authored link can't point curl at an arbitrary host) and keep it only
# if genuinely unreachable.
while IFS= read -r url; do
[ -z "$url" ] && continue
case "$url" in
"$PREVIEW_ORIGIN"/*|"$PREVIEW_ORIGIN")
code=$(curl -s -L -o /dev/null -w "%{http_code}" --max-time 20 "$url" 2>/dev/null)
{ [ -n "$code" ] && [ "$code" -ge 200 ] && [ "$code" -lt 400 ]; } && continue
report_timeout "$url" "timeout, recheck ${code:-000}"
;;
*)
report_timeout "$url" "timeout"
;;
esac
done <<< "$(jq -r '[.links[] | select(.status == 0) | .url] | unique | .[]' linkinator_results.json)"
if [ -n "$BROKEN_LINKS" ]; then
{
echo "**The following broken links were found:**"
echo ""
echo "$BROKEN_LINKS"
} >> preview_url.txt
# Fail the job AFTER the comment is posted (see the Fail step below).
echo "true" > check_failed.flag
else
echo "No broken links found" >> preview_url.txt
fi
rm -f linkinator_results.json
{
echo ""
echo "---"
echo "📌 Results for commit ${{ github.sha }}"
} >> preview_url.txt
- name: Comment URL in PR
if: always()
uses: thollander/actions-comment-pull-request@v3
with:
file-path: apps/opik-documentation/documentation/preview_url.txt
comment-tag: docs-preview
- name: Fail if the link check failed
if: always()
working-directory: apps/opik-documentation/documentation
run: |
if [ -f check_failed.flag ]; then
echo "Link check failed — broken links were found, or the crawl did not complete. See the PR comment."
exit 1
fi
@@ -0,0 +1,425 @@
name: E2E Tests - Post Merge (v2)
run-name: "E2E Tests v2 (${{ github.event.inputs.tier || 't1' }}) - ${{ github.event_name == 'push' && format('Post-merge {0}', github.sha) || format('Manual run by @{0}', github.actor) }}"
on:
push:
branches:
- 'main'
# Skip workflow if only documentation or unrelated files changed
paths-ignore:
- '**.md'
- 'apps/opik-documentation/**'
pull_request:
paths:
- '.github/workflows/e2e_tests_post_merge_v2.yml'
workflow_dispatch:
inputs:
tier:
type: choice
description: 'Which tag tier to run (t1=smoke, t2=cuj, t3=nightly)'
required: true
default: 't1'
options:
- t1
- t2
- t3
notify_on_success:
type: boolean
description: 'Send Slack notification even on success'
required: false
default: false
ALLURE_JOB_RUN_ID:
description: 'ALLURE_JOB_RUN_ID service parameter. Leave blank.'
required: false
env:
ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }}
ALLURE_ENDPOINT: https://comet.testops.cloud/
ALLURE_PROJECT_ID: 1
ALLURE_RESULTS: allure-results
ALLURE_JOB_RUN_ID: ${{ github.event.inputs.ALLURE_JOB_RUN_ID }}
OPIK_SENTRY_ENABLE: false
permissions:
contents: read
actions: read
jobs:
e2e-tests:
name: "🧪 E2E v2 Tests (${{ github.event.inputs.tier || 't1' }})"
runs-on: ubuntu-latest
timeout-minutes: 45
outputs:
test_result: ${{ steps.run-tests.outcome }}
testops_url: ${{ steps.set-outputs.outputs.testops_url }}
total_tests: ${{ steps.parse-results.outputs.total }}
passed_tests: ${{ steps.parse-results.outputs.passed }}
failed_tests: ${{ steps.parse-results.outputs.failed }}
skipped_tests: ${{ steps.parse-results.outputs.skipped }}
pr_author: ${{ steps.get-pr-info.outputs.author }}
steps:
# ========================================
# PHASE 1: Checkout & Environment Setup
# ========================================
- name: "📥 Checkout repository"
uses: actions/checkout@v6
- name: "🔍 Get PR/commit information"
id: get-pr-info
run: |
if [ "${{ github.event_name }}" == "push" ]; then
# For push events, get the author from the commit
AUTHOR="${{ github.event.head_commit.author.username }}"
if [ -z "$AUTHOR" ]; then
AUTHOR="${{ github.actor }}"
fi
else
AUTHOR="${{ github.actor }}"
fi
echo "author=$AUTHOR" >> "$GITHUB_OUTPUT"
echo "📝 Author: $AUTHOR"
- name: "📦 Setup Node.js"
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: tests_end_to_end/e2e/package-lock.json
- name: "🐍 Setup Python"
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: "⚡ Setup uv"
uses: astral-sh/setup-uv@v5
- name: "📚 Install E2E test dependencies"
working-directory: tests_end_to_end/e2e
run: |
npm ci
npx playwright install --with-deps chromium
- name: "📚 Sync bridge dependencies"
working-directory: tests_end_to_end/e2e/services/opik-sdk-driver
run: uv sync
- name: "📊 Install allurectl"
uses: allure-framework/setup-allurectl@v1
# ========================================
# PHASE 2: Build & Start Opik
# ========================================
- name: "🔨 Build and start Opik server"
env:
OPIK_USAGE_REPORT_ENABLED: false
COMPOSE_BAKE: false
run: |
echo "::group::Building Opik from branch: ${{ github.ref_name }}"
cd ${{ github.workspace }}
TOGGLE_WELCOME_WIZARD_ENABLED="false" TOGGLE_FORCE_WORKSPACE_VERSION="version_2" ./opik.sh --build
echo "::endgroup::"
echo "✅ Opik build complete"
- name: "🔍 Verify Docker pods are running"
run: |
echo "::group::Checking Docker pods"
chmod +x ./tests_end_to_end/installer_utils/check_docker_compose_pods.sh
./tests_end_to_end/installer_utils/check_docker_compose_pods.sh
echo "::endgroup::"
echo "✅ All Docker pods are running"
- name: "🏥 Verify backend health"
run: |
echo "::group::Checking backend health"
chmod +x ./tests_end_to_end/installer_utils/check_backend.sh
./tests_end_to_end/installer_utils/check_backend.sh
echo "::endgroup::"
echo "✅ Backend is healthy"
# ========================================
# PHASE 3: Run E2E Tests
# ========================================
- name: "🧪 Run E2E test suite"
id: run-tests
working-directory: tests_end_to_end/e2e
env:
OPIK_DEPLOYMENT: oss
OPIK_BASE_URL: http://localhost:5173
OPIK_WORKSPACE: default
WORKERS: 2
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ALLURE_LAUNCH_NAME: "Opik v2 Post-Merge ${{ github.event.inputs.tier || 't1' }} - ${{ github.run_id }}"
run: |
TIER="${{ github.event.inputs.tier || 't1' }}"
echo "========================================"
echo "🧪 Running v2 tier: $TIER"
echo "========================================"
echo ""
# Point both the allure-playwright reporter and allurectl at the same
# absolute results dir, then stream results to TestOps via allurectl watch.
export ALLURE_RESULTS="${{ github.workspace }}/tests_end_to_end/e2e/${{ env.ALLURE_RESULTS }}"
# Create .allure directory to prevent testplan.json error
mkdir -p .allure
# Run tests with allurectl watch for TestOps integration
# Capture output to extract the direct report URL
set +e
allurectl watch -- npm run test:${TIER} 2>&1 | tee allure_output.log
TEST_EXIT_CODE=${PIPESTATUS[0]}
set -e
# Extract the direct job run URL from allurectl output
if grep -q "Report link:" allure_output.log; then
REPORT_URL=$(grep "Report link:" allure_output.log | head -1 | sed 's/.*Report link:[[:space:]]*//' | awk '{print $1}')
echo "allure_report_url=${REPORT_URL}" >> "$GITHUB_OUTPUT"
echo "📊 Captured Allure TestOps report URL: ${REPORT_URL}"
else
echo "allure_report_url=" >> "$GITHUB_OUTPUT"
echo "⚠️ Could not capture direct report URL from allurectl output"
fi
echo ""
echo "========================================"
echo "Test run completed with exit code: $TEST_EXIT_CODE"
echo "========================================"
exit "$TEST_EXIT_CODE"
# ========================================
# PHASE 4: Process Results
# ========================================
- name: "📊 Parse test results"
id: parse-results
if: always()
working-directory: tests_end_to_end/e2e
run: |
echo "::group::Parsing test results"
if [ -f "test-results/results.json" ]; then
echo "Found results.json, parsing..."
TOTAL=$(jq '.stats.expected + .stats.unexpected + .stats.flaky + .stats.skipped' test-results/results.json 2>/dev/null || echo "0")
PASSED=$(jq '.stats.expected' test-results/results.json 2>/dev/null || echo "0")
FAILED=$(jq '.stats.unexpected' test-results/results.json 2>/dev/null || echo "0")
SKIPPED=$(jq '.stats.skipped' test-results/results.json 2>/dev/null || echo "0")
FLAKY=$(jq '.stats.flaky' test-results/results.json 2>/dev/null || echo "0")
echo "Results: Total=$TOTAL, Passed=$PASSED, Failed=$FAILED, Skipped=$SKIPPED, Flaky=$FLAKY"
else
echo "⚠️ results.json not found, using defaults"
TOTAL="0"
PASSED="0"
FAILED="0"
SKIPPED="0"
FLAKY="0"
fi
{
echo "total=$TOTAL"
echo "passed=$PASSED"
echo "failed=$FAILED"
echo "skipped=$SKIPPED"
echo "flaky=$FLAKY"
} >> "$GITHUB_OUTPUT"
echo "::endgroup::"
- name: "🔗 Set output URLs"
id: set-outputs
if: always()
run: |
# Use direct report URL if captured, otherwise fall back to launches page
DIRECT_URL="${{ steps.run-tests.outputs.allure_report_url }}"
if [ -n "$DIRECT_URL" ]; then
TESTOPS_URL="$DIRECT_URL"
echo "📊 Using direct TestOps report URL"
else
TESTOPS_URL="${{ env.ALLURE_ENDPOINT }}project/${{ env.ALLURE_PROJECT_ID }}/launches"
echo "📊 Using fallback launches page URL"
fi
echo "testops_url=$TESTOPS_URL" >> "$GITHUB_OUTPUT"
# ========================================
# PHASE 5: Generate Summary
# ========================================
- name: "📋 Generate job summary"
if: always()
run: |
TIER="${{ github.event.inputs.tier || 't1' }}"
TESTOPS_URL="${{ steps.set-outputs.outputs.testops_url }}"
TOTAL="${{ steps.parse-results.outputs.total }}"
PASSED="${{ steps.parse-results.outputs.passed }}"
FAILED="${{ steps.parse-results.outputs.failed }}"
SKIPPED="${{ steps.parse-results.outputs.skipped }}"
# Redirect stdout to the summary file for the remainder of this step
exec >> "$GITHUB_STEP_SUMMARY"
# Header
echo "# 🧪 E2E v2 Test Results"
echo ""
# Status banner
if [ "${{ steps.run-tests.outcome }}" == "success" ]; then
echo "> ✅ **All tests passed!**"
elif [ "${{ steps.run-tests.outcome }}" == "failure" ]; then
echo "> ❌ **$FAILED test(s) failed** - Please review the results below"
else
echo "> ⚠️ **Test run status: ${{ steps.run-tests.outcome }}**"
fi
echo ""
# Configuration table
echo "## 📝 Configuration"
echo ""
echo "| Property | Value |"
echo "|----------|-------|"
echo "| **Tier** | \`$TIER\` |"
echo "| **Branch** | \`${{ github.ref_name }}\` |"
echo "| **Commit** | [\`${GITHUB_SHA:0:7}\`](https://github.com/${{ github.repository }}/commit/${{ github.sha }}) |"
echo "| **Trigger** | ${{ github.event_name == 'push' && 'Post-merge (push to main)' || 'Manual dispatch' }} |"
echo "| **Author** | @${{ steps.get-pr-info.outputs.author }} |"
echo ""
# Results table
echo "## 📊 Results"
echo ""
echo "| Status | Count |"
echo "|--------|------:|"
echo "| ✅ Passed | $PASSED |"
echo "| ❌ Failed | $FAILED |"
echo "| ⏭️ Skipped | $SKIPPED |"
echo "| **Total** | **$TOTAL** |"
echo ""
# Links section
echo "## 🔗 Quick Links"
echo ""
echo "| Resource | Link |"
echo "|----------|------|"
echo "| 📊 **TestOps Dashboard** | [View detailed results, traces & history]($TESTOPS_URL) |"
echo "| 📄 **Playwright Report** | [Download from Artifacts below](#artifacts) |"
echo ""
# Debugging section (only on failure)
if [ "${{ steps.run-tests.outcome }}" == "failure" ]; then
echo "## 🔧 Debugging Guide"
echo ""
echo "### Quick steps to investigate failures:"
echo ""
echo "1. **Open TestOps** → Click the link above to see detailed failure traces, screenshots, and video recordings"
echo "2. **Download Playwright Report** → The HTML report includes step-by-step traces for each test"
echo "3. **Check test logs** → Expand the 'Run E2E test suite' step above for console output"
echo ""
echo "### Common failure causes:"
echo "- UI element selectors changed (missing data-testid)"
echo "- API response format changed"
echo "- Timing/race conditions"
echo "- New feature requiring test updates"
fi
# ========================================
# PHASE 6: Upload Artifacts
# ========================================
- name: "📤 Upload Allure results"
if: always()
uses: actions/upload-artifact@v7
with:
name: allure-results-v2
path: tests_end_to_end/e2e/allure-results
retention-days: 14
- name: "📤 Upload Playwright report"
if: always()
uses: actions/upload-artifact@v7
with:
name: playwright-report-v2
path: tests_end_to_end/e2e/playwright-report
retention-days: 14
- name: "📤 Upload test results JSON"
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-json-v2
path: tests_end_to_end/e2e/test-results/results.json
retention-days: 14
if-no-files-found: ignore
- name: "🧹 Stop Opik server"
if: always()
run: |
echo "Stopping Opik server..."
./opik.sh --stop || true
echo "✅ Cleanup complete"
notify-slack:
name: "📢 Slack Notification"
runs-on: ubuntu-latest
needs: e2e-tests
if: |
always() &&
needs.e2e-tests.result != 'cancelled' &&
(needs.e2e-tests.result == 'failure' || github.event.inputs.notify_on_success == 'true')
steps:
- name: Checkout
uses: actions/checkout@v6
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: false
- name: Resolve Slack mentions
id: resolve-slack-mentions
env:
SLACK_USER_MAPPING: ${{ secrets.SLACK_USER_MAPPING }}
SLACK_QA_LEAD_ID: ${{ secrets.SLACK_QA_LEAD_ID }}
run: |
AUTHOR="${{ needs.e2e-tests.outputs.pr_author }}"
IS_FAILURE="${{ needs.e2e-tests.result == 'failure' }}"
if [ "$IS_FAILURE" == "true" ]; then
if [ -n "$SLACK_QA_LEAD_ID" ]; then
QA_MENTION="<@$SLACK_QA_LEAD_ID>"
else
QA_MENTION=""
fi
AUTHOR_MENTION=""
if [ -n "$SLACK_USER_MAPPING" ]; then
AUTHOR_SLACK_ID=$(echo "$SLACK_USER_MAPPING" | jq -r --arg user "$AUTHOR" '.[$user] // empty' 2>/dev/null || echo "")
if [ -n "$AUTHOR_SLACK_ID" ] && [ "$AUTHOR_SLACK_ID" != "$SLACK_QA_LEAD_ID" ]; then
AUTHOR_MENTION="<@$AUTHOR_SLACK_ID>"
fi
fi
MENTIONS="${QA_MENTION:+$QA_MENTION }${AUTHOR_MENTION}"
echo "mentions=${MENTIONS% }" >> "$GITHUB_OUTPUT"
else
echo "mentions=" >> "$GITHUB_OUTPUT"
fi
echo "author_display=$AUTHOR" >> "$GITHUB_OUTPUT"
- name: Send Slack notification
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
TEST_RESULT: ${{ needs.e2e-tests.result }}
SUITE_NAME: "v2 ${{ github.event.inputs.tier || 't1' }}"
PASSED_TESTS: ${{ needs.e2e-tests.outputs.passed_tests }}
FAILED_TESTS: ${{ needs.e2e-tests.outputs.failed_tests }}
TESTOPS_URL: ${{ needs.e2e-tests.outputs.testops_url }}
AUTHOR_DISPLAY: ${{ steps.resolve-slack-mentions.outputs.author_display }}
MENTIONS: ${{ steps.resolve-slack-mentions.outputs.mentions }}
run: .github/scripts/notify-slack-e2e-post-merge.sh
+147
View File
@@ -0,0 +1,147 @@
name: Application E2E Tests (v2)
env:
ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }}
ALLURE_JOB_RUN_ID: ${{ github.event.inputs.ALLURE_JOB_RUN_ID }}
ALLURE_ENDPOINT: https://comet.testops.cloud/
ALLURE_PROJECT_ID: 1
ALLURE_RESULTS: allure-results
on:
workflow_dispatch:
inputs:
tier:
type: choice
description: 'Which tag tier to run'
required: true
default: 't1'
options:
- t1
- t2
- t3
ALLURE_JOB_RUN_ID:
description: ALLURE_JOB_RUN_ID service parameter. Leave blank.
ALLURE_USERNAME:
description: ALLURE_USERNAME service parameter. Leave blank.
pull_request:
paths:
- 'sdks/code_generation/fern/openapi/openapi.yaml'
- 'tests_end_to_end/e2e/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
run-name: Application E2E Tests (v2) - ${{ github.event.inputs.tier || 't1' }}
permissions:
contents: read
jobs:
run_suite:
name: "Run v2 suite: ${{ github.event.inputs.tier || 't1' }}"
runs-on: ubuntu-latest
timeout-minutes: 30
env:
OPIK_DEPLOYMENT: oss
OPIK_BASE_URL: http://localhost:5173
OPIK_WORKSPACE: default
WORKERS: 2
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: tests_end_to_end/e2e/package-lock.json
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Setup uv
uses: astral-sh/setup-uv@v5
- name: Install E2E test dependencies
working-directory: ${{ github.workspace }}/tests_end_to_end/e2e
run: |
npm ci
npx playwright install --with-deps chromium
- name: Sync bridge dependencies
working-directory: ${{ github.workspace }}/tests_end_to_end/e2e/services/opik-sdk-driver
run: uv sync
- name: Install allurectl
uses: allure-framework/setup-allurectl@v1
- name: Install Opik (Local)
env:
OPIK_USAGE_REPORT_ENABLED: false
# Avoid Buildx Bake concurrent image export collisions in CI.
COMPOSE_BAKE: false
run: |
cd ${{ github.workspace }}
TOGGLE_WELCOME_WIZARD_ENABLED="false" TOGGLE_FORCE_WORKSPACE_VERSION="version_2" ./opik.sh --build
- name: Check Docker pods are up (Local)
run: |
chmod +x ./tests_end_to_end/installer_utils/check_docker_compose_pods.sh
./tests_end_to_end/installer_utils/check_docker_compose_pods.sh
shell: bash
- name: Check backend health (Local)
run: |
chmod +x ./tests_end_to_end/installer_utils/check_backend.sh
./tests_end_to_end/installer_utils/check_backend.sh
shell: bash
- name: Run v2 E2E suite
working-directory: ${{ github.workspace }}/tests_end_to_end/e2e
env:
ALLURE_LAUNCH_NAME: "Opik v2 E2E ${{ github.event.inputs.tier || 't1' }} - ${{ github.run_id }}"
run: |
# Point both the allure-playwright reporter and allurectl at the same
# absolute results dir, then stream results to TestOps via allurectl watch.
export ALLURE_RESULTS="${{ github.workspace }}/tests_end_to_end/e2e/allure-results"
allurectl watch -- npm run test:${{ github.event.inputs.tier || 't1' }}
- name: Upload Allure Results
if: always()
uses: actions/upload-artifact@v7
with:
name: allure-results-v2
path: tests_end_to_end/e2e/allure-results
retention-days: 7
- name: Upload Playwright Report
if: always()
uses: actions/upload-artifact@v7
with:
name: playwright-report-v2
path: tests_end_to_end/e2e/playwright-report
retention-days: 7
- name: Upload Test Results (traces, videos)
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-v2
path: tests_end_to_end/e2e/test-results
retention-days: 7
- name: Stop Opik server (Local)
if: always()
run: |
cd ${{ github.workspace }}
./opik.sh --stop
continue-on-error: ${{ github.event_name == 'pull_request' }}
+46
View File
@@ -0,0 +1,46 @@
name: Frontend Unit Tests
run-name: "Frontend Unit Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
on:
pull_request:
paths:
- "apps/opik-frontend/**"
push:
branches:
- 'main'
paths:
- "apps/opik-frontend/**"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
tests:
name: Test on Node ${{ matrix.node-version }}
runs-on: ubuntu-latest
timeout-minutes: 5
defaults:
run:
working-directory: apps/opik-frontend
strategy:
fail-fast: false
matrix:
node-version: [ 20 ]
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v4.2.0
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Unit tests
run: npm run test
+157
View File
@@ -0,0 +1,157 @@
name: Guardrails E2E Tests
run-name: "Guardrails E2E Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
checks: write
pull-requests: write
on:
workflow_dispatch:
pull_request:
paths:
- 'apps/opik-guardrails-backend/**'
- 'sdks/python/src/opik/guardrails/**'
- 'sdks/python/tests/e2e/test_guardrails.py'
- '.github/workflows/guardrails_e2e_tests.yml'
push:
branches:
- 'main'
paths:
- 'apps/opik-guardrails-backend/**'
- 'apps/opik-backend/**'
- 'sdks/python/src/opik/**'
- 'sdks/python/tests/e2e/test_guardrails.py'
- '.github/workflows/guardrails_e2e_tests.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
OPIK_URL_OVERRIDE: http://localhost:8080
OPIK_GUARDRAILS_URL_OVERRIDE: http://localhost:5000
OPIK_CONSOLE_LOGGING_LEVEL: DEBUG
jobs:
select-matrix:
name: Select Python version matrix
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
python_versions: ${{ steps.pick.outputs.versions }}
steps:
- uses: actions/checkout@v6
with:
sparse-checkout: .github/actions/select-e2e-matrix
sparse-checkout-cone-mode: false
- uses: ./.github/actions/select-e2e-matrix
id: pick
with:
all-versions: ${{ vars.PYTHON_VERSIONS }}
match-regex: '^apps/opik-guardrails-backend/|^sdks/python/src/opik/guardrails/|^sdks/python/tests/e2e/test_guardrails\.py$|^\.github/workflows/guardrails_e2e_tests\.yml$'
github-token: ${{ secrets.GITHUB_TOKEN }}
e2e-tests:
needs: select-matrix
name: Guardrails E2E Tests ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
python_version: ${{ fromJSON(needs.select-matrix.outputs.python_versions) }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Run latest Opik server with guardrails
env:
OPIK_USAGE_REPORT_ENABLED: false
COMPOSE_BAKE: false
TOGGLE_RUNNERS_ENABLED: "true"
run: |
cd ${{ github.workspace }}
./opik.sh --backend --port-mapping --guardrails --build
- name: Check Opik server availability
shell: bash
run: |
chmod +x ${{ github.workspace }}/tests_end_to_end/installer_utils/*.sh
cd ${{ github.workspace }}/deployment/docker-compose
echo "Check Docker pods are up"
${{ github.workspace }}/tests_end_to_end/installer_utils/check_docker_compose_pods.sh
echo "Check backend health"
${{ github.workspace }}/tests_end_to_end/installer_utils/check_backend.sh
- name: Install opik SDK
run: |
cd ${{ github.workspace }}/sdks/python
pip install .
- name: Install test requirements
run: |
cd ${{ github.workspace }}/sdks/python
pip install -r tests/test_requirements.txt
pip list
- name: Run guardrails E2E tests
run: |
cd ${{ github.workspace }}/sdks/python
pytest tests/e2e/test_guardrails.py -vv --durations=20 --junitxml=${{ github.workspace }}/test_results_guardrails_${{matrix.python_version}}.xml
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action/linux@v2
if: always()
with:
action_fail: true
comment_mode: failures
check_name: Guardrails E2E Tests Results (Python ${{matrix.python_version}})
files: ${{ github.workspace }}/test_results_guardrails_${{matrix.python_version}}.xml
- name: Keep BE log in case of failure
if: failure()
run: |
docker logs opik-backend-1 > ${{ github.workspace }}/opik-backend_guardrails_p${{matrix.python_version}}.log || true
docker logs opik-guardrails-backend-1 > ${{ github.workspace }}/opik-guardrails-backend_p${{matrix.python_version}}.log || true
- name: Attach BE log
if: failure()
uses: actions/upload-artifact@v7
with:
name: opik-guardrails-logs-p${{matrix.python_version}}
path: |
${{ github.workspace }}/opik-backend_guardrails_p${{matrix.python_version}}.log
${{ github.workspace }}/opik-guardrails-backend_p${{matrix.python_version}}.log
- name: Stop opik server
if: always()
run: |
cd ${{ github.workspace }}
./opik.sh --stop --guardrails
notify-slack:
name: "📢 Slack Notification"
runs-on: ubuntu-latest
needs: e2e-tests
if: always() && needs.e2e-tests.result == 'failure' && github.event_name != 'pull_request'
steps:
- name: Checkout
uses: actions/checkout@v6
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: false
- name: Send Slack failure notification
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
run: .github/scripts/notify-slack-test-failure.sh "Guardrails E2E Tests"
@@ -0,0 +1,78 @@
name: "Guardrails Backend Unit Tests"
run-name: "Guardrails Backend Unit Tests on ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
checks: write
pull-requests: write
on:
workflow_dispatch:
pull_request:
paths:
- 'apps/opik-guardrails-backend/**'
- '.github/workflows/guardrails_unit_tests.yml'
push:
branches:
- 'main'
paths:
- 'apps/opik-guardrails-backend/**'
- '.github/workflows/guardrails_unit_tests.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
unit-tests:
name: Guardrails Backend Unit Tests
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: apps/opik-guardrails-backend/
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt -r tests/test_requirements.txt
- name: Run unit tests
run: |
cd tests && pytest unit -v --junitxml=../test-results.xml
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action/linux@v2
if: always()
with:
action_fail: true
comment_mode: failures
check_name: Guardrails Backend Unit Tests Results
files: apps/opik-guardrails-backend/test-results.xml
notify-slack:
name: "📢 Slack Notification"
runs-on: ubuntu-latest
needs: unit-tests
if: always() && needs.unit-tests.result == 'failure' && github.event_name != 'pull_request'
steps:
- name: Checkout
uses: actions/checkout@v6
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: false
- name: Send Slack failure notification
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
run: .github/scripts/notify-slack-test-failure.sh "Guardrails Backend Unit Tests"
+267
View File
@@ -0,0 +1,267 @@
name: Installation Tests
on:
schedule:
- cron: '0 12 * * *' # runs every day at noon UTC
workflow_dispatch:
jobs:
test-opiksh:
name: opik.sh (main README)
runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: false
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install Python dependencies
run: |
pip install -r ${{ github.workspace }}/tests_end_to_end/test_requirements.txt
pip install opik
playwright install chromium
- name: Install using opik.sh
env:
OPIK_USAGE_REPORT_ENABLED: false
run: |
cd ${{ github.workspace }}
./opik.sh
- name: Run trace test
run: |
cd ${{ github.workspace }}/tests_end_to_end
export PYTHONPATH='.'
pytest -xvs tests/Installation/test_trace_logging.py --browser chromium
- name: Stop Opik
if: always()
run: |
cd ${{ github.workspace }}
./opik.sh --stop
test-docker-compose-up:
name: docker compose up --detach (Comet docs)
runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: false
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Python dependencies
run: |
pip install -r ${{ github.workspace }}/tests_end_to_end/test_requirements.txt
pip install opik
playwright install chromium
- name: Install using docker compose up --detach
env:
OPIK_USAGE_REPORT_ENABLED: false
run: |
cd ${{ github.workspace }}/deployment/docker-compose
docker compose --profile opik up --detach
- name: Check Docker pods are up (Local)
run: |
chmod +x ./tests_end_to_end/installer_utils/check_docker_compose_pods.sh
./tests_end_to_end/installer_utils/check_docker_compose_pods.sh
shell: bash
- name: Check backend health (Local)
run: |
chmod +x ./tests_end_to_end/installer_utils/check_backend.sh
./tests_end_to_end/installer_utils/check_backend.sh
shell: bash
- name: Run trace test
run: |
cd ${{ github.workspace }}/tests_end_to_end
export PYTHONPATH='.'
pytest -xvs tests/Installation/test_trace_logging.py --browser chromium
- name: Stop Opik
if: always()
run: |
cd ${{ github.workspace }}/deployment/docker-compose
docker compose --profile opik down
test-docker-compose-build:
name: docker compose up --build (Comet docs)
runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: false
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Python dependencies
run: |
pip install -r ${{ github.workspace }}/tests_end_to_end/test_requirements.txt
pip install opik
playwright install chromium
- name: Install with --build flag
env:
OPIK_USAGE_REPORT_ENABLED: false
run: |
cd ${{ github.workspace }}/deployment/docker-compose
docker compose --profile opik up -d --build
- name: Check Docker pods are up (Local)
run: |
chmod +x ./tests_end_to_end/installer_utils/check_docker_compose_pods.sh
./tests_end_to_end/installer_utils/check_docker_compose_pods.sh
shell: bash
- name: Check backend health (Local)
run: |
chmod +x ./tests_end_to_end/installer_utils/check_backend.sh
./tests_end_to_end/installer_utils/check_backend.sh
shell: bash
- name: Run trace test
run: |
cd ${{ github.workspace }}/tests_end_to_end
export PYTHONPATH='.'
pytest -xvs tests/Installation/test_trace_logging.py --browser chromium
- name: Stop Opik
if: always()
run: |
cd ${{ github.workspace }}/deployment/docker-compose
docker compose --profile opik down
test-docker-compose-with-version:
name: Set version - docker compose (deployment README)
runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: false
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Python dependencies
run: |
pip install -r ${{ github.workspace }}/tests_end_to_end/test_requirements.txt
pip install opik
playwright install chromium
- name: Install with docker compose with specific version set
env:
OPIK_USAGE_REPORT_ENABLED: false
run: |
cd ${{ github.workspace }}/deployment/docker-compose
export OPIK_VERSION=1.6.5
docker compose -f docker-compose.yaml --profile opik up -d
- name: Check Docker pods are up (Local)
run: |
chmod +x ./tests_end_to_end/installer_utils/check_docker_compose_pods.sh
./tests_end_to_end/installer_utils/check_docker_compose_pods.sh
shell: bash
- name: Check backend health (Local)
run: |
chmod +x ./tests_end_to_end/installer_utils/check_backend.sh
./tests_end_to_end/installer_utils/check_backend.sh
shell: bash
- name: Run trace test
run: |
cd ${{ github.workspace }}/tests_end_to_end
export PYTHONPATH='.'
pytest -xvs tests/Installation/test_trace_logging.py --browser chromium
- name: Stop Opik
if: always()
run: |
cd ${{ github.workspace }}/deployment/docker-compose
docker compose --profile opik down
test-docker-compose-with-pull:
name: Docker compose pull + up (Deployment README)
runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: false
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Python dependencies
run: |
pip install -r ${{ github.workspace }}/tests_end_to_end/test_requirements.txt
pip install opik
playwright install chromium
- name: Install with docker compose after pulling latest version
env:
OPIK_USAGE_REPORT_ENABLED: false
run: |
cd ${{ github.workspace }}/deployment/docker-compose
docker compose --profile opik pull
docker compose -f docker-compose.yaml --profile opik up -d
- name: Check Docker pods are up (Local)
run: |
chmod +x ./tests_end_to_end/installer_utils/check_docker_compose_pods.sh
./tests_end_to_end/installer_utils/check_docker_compose_pods.sh
shell: bash
- name: Check backend health (Local)
run: |
chmod +x ./tests_end_to_end/installer_utils/check_backend.sh
./tests_end_to_end/installer_utils/check_backend.sh
shell: bash
- name: Run trace test
run: |
cd ${{ github.workspace }}/tests_end_to_end
export PYTHONPATH='.'
pytest -xvs tests/Installation/test_trace_logging.py --browser chromium
- name: Stop Opik
if: always()
run: |
cd ${{ github.workspace }}/deployment/docker-compose
docker compose --profile opik down
+29
View File
@@ -0,0 +1,29 @@
name: 🗂️ Auto Label PR
on:
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
labeler:
name: PR Labeler - Apply and Sync Labels
runs-on: ubuntu-latest
timeout-minutes: 1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: read
pull-requests: write
steps:
- name: Apply and Sync Labels
uses: actions/labeler@v5
with:
sync-labels: true # Remove labels if the file paths no longer match
configuration-path: .github/labeler.yml
@@ -0,0 +1,69 @@
# Workflow to run ADK tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib ADK (Google) Tests
run-name: "SDK Lib ADK Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
GCP_CREDENTIALS_JSON: ${{ secrets.GCP_CREDENTIALS_JSON }}
GOOGLE_CLOUD_LOCATION: us-east1
GOOGLE_CLOUD_PROJECT: opik-sdk-tests
GOOGLE_GENAI_USE_VERTEXAI: TRUE
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
on:
workflow_call:
jobs:
tests:
name: ADK Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
# Throttle concurrent matrix jobs so we keep full Python coverage
# without re-multiplying live Vertex AI calls into 429 RESOURCE_EXHAUSTED
# against the shared opik-sdk-tests GCP project.
max-parallel: 2
matrix:
python_version: ${{ fromJSON(vars.PYTHON_VERSIONS) }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/adk/requirements_legacy_adk_1_3_0.txt
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_CREDENTIALS_JSON }}
- name: Run tests
run: |
cd ./tests/library_integration/adk/
python -m pytest -vv .
+72
View File
@@ -0,0 +1,72 @@
# Workflow to run ADK tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib ADK (Google) Tests
run-name: "SDK Lib ADK Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
GCP_CREDENTIALS_JSON: ${{ secrets.GCP_CREDENTIALS_JSON }}
GOOGLE_CLOUD_LOCATION: us-east1
GOOGLE_CLOUD_PROJECT: opik-sdk-tests
GOOGLE_GENAI_USE_VERTEXAI: TRUE
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
on:
workflow_call:
jobs:
tests:
name: ADK Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
# This suite makes live Vertex AI calls against the shared
# opik-sdk-tests GCP project (shared with the GenAI suite). Running the
# full 3.10-3.14 matrix re-multiplied those calls into 429
# RESOURCE_EXHAUSTED, so we run only the oldest and newest supported
# Python versions — enough to catch version-specific breakage at a
# fraction of the request volume — and keep the concurrency cap.
max-parallel: 2
matrix:
python_version: ["3.10", "3.14"]
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/adk/requirements.txt
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_CREDENTIALS_JSON }}
- name: Run tests
run: |
cd ./tests/library_integration/adk/
python -m pytest -vv .
+59
View File
@@ -0,0 +1,59 @@
# Workflow to run AISuite tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib AISuite Tests
run-name: "SDK Lib AISuite Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
on:
workflow_call:
jobs:
tests:
name: AISuite Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
matrix:
# 3.14 is not supported by aisuite's docstring_parser dependency
# https://github.com/andrewyng/aisuite/issues/265
python_version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/aisuite/requirements.txt
- name: Run tests
run: |
cd ./tests/library_integration/aisuite/
python -m pytest -vv .
+55
View File
@@ -0,0 +1,55 @@
# Workflow to run Anthropic tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib Anthropic Tests
run-name: "SDK Lib Anthropic Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
on:
workflow_call:
jobs:
tests:
name: Anthropic Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
matrix:
python_version: ${{ fromJSON(vars.PYTHON_VERSIONS) }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/anthropic/requirements.txt
- name: Run tests
run: |
cd ./tests/library_integration/anthropic/
python -m pytest -vv .
+67
View File
@@ -0,0 +1,67 @@
# Workflow to run Bedrock tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib Bedrock Tests
run-name: "SDK Lib Bedrock Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
AWS_DEFAULT_REGION: us-east-1
AWS_ACCESS_KEY_ID: ${{ secrets.BEDROCK_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.BEDROCK_AWS_SECRET_ACCESS_KEY }}
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
on:
workflow_call:
jobs:
tests:
name: Bedrock Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
matrix:
python_version: ${{ fromJSON(vars.PYTHON_VERSIONS) }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/bedrock/requirements.txt
- name: change aws role
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.BEDROCK_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.BEDROCK_AWS_SECRET_ACCESS_KEY }}
role-to-assume: ${{ vars.BEDROCK_ROLE }}
aws-region: us-east-1
role-chaining: true
role-skip-session-tagging: true
- name: Run tests
run: |
cd ./tests/library_integration/bedrock/
python -m pytest -vv .
+75
View File
@@ -0,0 +1,75 @@
# Workflow to run CrewAI v0.x tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib CrewAI v0 Tests
run-name: "SDK Lib CrewAI v0 Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
AWS_DEFAULT_REGION: us-east-1
AWS_ACCESS_KEY_ID: ${{ secrets.BEDROCK_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.BEDROCK_AWS_SECRET_ACCESS_KEY }}
GCP_CREDENTIALS_JSON: ${{ secrets.GCP_CREDENTIALS_JSON }}
GOOGLE_CLOUD_LOCATION: us-east1
GOOGLE_CLOUD_PROJECT: opik-sdk-tests
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
on:
workflow_call:
jobs:
tests:
name: CrewAI v0 Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
matrix:
# 3.14+ is not supported by crewai v0.x
python_version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/crewai/requirements_v0.txt
- name: Configure AWS credentials for Bedrock
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.BEDROCK_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.BEDROCK_AWS_SECRET_ACCESS_KEY }}
role-to-assume: ${{ vars.BEDROCK_ROLE }}
aws-region: us-east-1
role-chaining: true
role-skip-session-tagging: true
- name: Run tests
run: |
cd ./tests/library_integration/crewai/
python -m pytest -vv .
+75
View File
@@ -0,0 +1,75 @@
# Workflow to run CrewAI v1.0.0+ tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib CrewAI v1 Tests
run-name: "SDK Lib CrewAI v1 Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
AWS_DEFAULT_REGION: us-east-1
AWS_ACCESS_KEY_ID: ${{ secrets.BEDROCK_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.BEDROCK_AWS_SECRET_ACCESS_KEY }}
GCP_CREDENTIALS_JSON: ${{ secrets.GCP_CREDENTIALS_JSON }}
GOOGLE_CLOUD_LOCATION: us-east1
GOOGLE_CLOUD_PROJECT: opik-sdk-tests
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
on:
workflow_call:
jobs:
tests:
name: CrewAI v1 Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
matrix:
# 3.14+ is not supported by crewai v1.x
python_version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/crewai/requirements_v1.txt
- name: Configure AWS credentials for Bedrock
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.BEDROCK_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.BEDROCK_AWS_SECRET_ACCESS_KEY }}
role-to-assume: ${{ vars.BEDROCK_ROLE }}
aws-region: us-east-1
role-chaining: true
role-skip-session-tagging: true
- name: Run tests
run: |
cd ./tests/library_integration/crewai/
python -m pytest -vv .
+57
View File
@@ -0,0 +1,57 @@
# Workflow to run DSPy tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib DSPy Tests
run-name: "SDK Lib DSPy Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
on:
workflow_call:
jobs:
tests:
name: DSPy Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
matrix:
# 3.14 is supported in 3.1.0b1 and up. Once 3.1.0 is released, we can add 3.14 to the matrix.
python_version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/dspy/requirements.txt
- name: Run tests
run: |
cd ./tests/library_integration/dspy/
python -m pytest -vv .
+71
View File
@@ -0,0 +1,71 @@
# Workflow to run GenAI tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib GenAI (Google) Tests
run-name: "SDK Lib GenAI Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
GCP_CREDENTIALS_JSON: ${{ secrets.GCP_CREDENTIALS_JSON }}
GOOGLE_CLOUD_LOCATION: us-east1
GOOGLE_CLOUD_PROJECT: opik-sdk-tests
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
OPIK_TEST_EXPENSIVE: ${{ inputs.run_expensive_tests }}
on:
workflow_call:
inputs:
run_expensive_tests:
description: "Run expensive tests (e.g., video generation)"
required: false
type: boolean
default: false
jobs:
tests:
name: GenAI Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
# This suite makes live Vertex AI calls against the shared
# opik-sdk-tests GCP project (shared with the ADK suite). Running the
# full 3.10-3.14 matrix re-multiplied those calls into 429
# RESOURCE_EXHAUSTED, so we run only the oldest and newest supported
# Python versions — enough to catch version-specific breakage at a
# fraction of the request volume — and keep the concurrency cap.
max-parallel: 2
matrix:
python_version: ["3.10", "3.14"]
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/genai/requirements.txt
- name: Run tests
run: |
cd ./tests/library_integration/genai/
python -m pytest -vv .
+55
View File
@@ -0,0 +1,55 @@
# Workflow to run Groq tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib Groq Tests
run-name: "SDK Lib Groq Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
on:
workflow_call:
jobs:
tests:
name: Groq Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
matrix:
python_version: ${{ fromJSON(vars.PYTHON_VERSIONS) }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/groq/requirements.txt
- name: Run tests
run: |
cd ./tests/library_integration/groq/
python -m pytest -vv .
@@ -0,0 +1,62 @@
# Workflow to run Guar tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib Guardrails Tests
run-name: "SDK Lib Guardrails Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
GUARDRAILS_API_KEY: ${{ secrets.GUARDRAILS_API_KEY }}
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
on:
workflow_call:
jobs:
tests:
name: Guardrails Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
matrix:
python_version: ${{ fromJSON(vars.PYTHON_VERSIONS) }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/guardrails/requirements.txt
- name: Install checks from guardrails hub
run: |
guardrails configure --token "$GUARDRAILS_API_KEY" --disable-metrics --enable-remote-inferencing;
guardrails hub install hub://guardrails/politeness_check
- name: Run tests
run: |
cd ./tests/library_integration/guardrails/
python -m pytest -vv .
+54
View File
@@ -0,0 +1,54 @@
# Workflow to run Harbor tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib Harbor Tests
run-name: "SDK Lib Harbor Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
on:
workflow_call:
jobs:
tests:
name: Harbor Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
matrix:
python_version: ["3.12", "3.13"]
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/harbor/requirements.txt
- name: Run tests
run: |
cd ./tests/library_integration/harbor/
python -m pytest -vv .
+56
View File
@@ -0,0 +1,56 @@
# Workflow to run Haystack tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib Haystack Tests
run-name: "SDK Lib Haystack Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
on:
workflow_call:
jobs:
tests:
name: Haystack Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
matrix:
python_version: ${{ fromJSON(vars.PYTHON_VERSIONS) }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/haystack/requirements.txt
- name: Run tests
run: |
cd ./tests/library_integration/haystack/
python -m pytest -vv .
@@ -0,0 +1,303 @@
# Runner for the suite of library integration tests
#
# echo "result=${{ secrets.OPENAI_API_KEY != '' }}" >> $GITHUB_OUTPUT
name: SDK Library Integration Tests Runner
run-name: "SDK Library Integration Tests Runner ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
on:
workflow_dispatch:
inputs:
libs:
description: "Choose specific library to test against or all"
required: true
type: choice
options:
- all
- openai
- langchain
- langchain_legacy
- llama_index
- anthropic
- mistral
- groq
- aisuite
- haystack
# - guardrails # disabled: guardrails-ai is on PyPI quarantine
- dspy
- crewai_v0
- crewai_v1
- genai
- adk
- adk_legacy_1_3_0
- metrics
- bedrock
- litellm
- harbor
run_expensive_tests:
description: "Run expensive tests (e.g., video generation). Enabled by default on weekly scheduled runs."
required: false
type: boolean
default: false
schedule:
# Daily run at midnight UTC Monday-Saturday (without expensive tests)
- cron: "0 0 * * 1-6"
# Weekly run on Sunday at midnight UTC (with expensive tests)
- cron: "0 0 * * 0"
pull_request:
paths:
- 'sdks/python/**'
push:
branches:
- 'main'
paths:
- 'sdks/python/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
LIBS: ${{ github.event.inputs.libs != '' && github.event.inputs.libs || 'all' }}
OPIK_SENTRY_ENABLE: False
jobs:
has_needed_secrets:
name: Check Secrets
runs-on: ubuntu-latest
outputs:
has_secrets: ${{ steps.init.outputs.has_secrets }}
steps:
- name: Print has secrets into output
id: init
run: |
echo "has_secrets=${{ secrets.OPENAI_API_KEY != '' }}" >> "$GITHUB_OUTPUT"
missed_api_key_warning:
name: Missed OpenAI API Key Warning
needs: [has_needed_secrets]
runs-on: ubuntu-latest
if: ${{ needs.has_needed_secrets.outputs.has_secrets == 'false' }}
steps:
- name: Print disabled message
run: |
echo "::warning::SDK Library Integration Tests are disabled because OPENAI_API_KEY is not set"
init_environment:
name: Build
needs: [has_needed_secrets]
runs-on: ubuntu-latest
timeout-minutes: 30
if: ${{ needs.has_needed_secrets.outputs.has_secrets == 'true' }}
outputs:
LIBS: ${{ steps.init.outputs.LIBS }}
steps:
- name: Make LIBS variable global (workaround for cron)
id: init
run: |
echo "LIBS=${{ env.LIBS }}" >> "$GITHUB_OUTPUT"
openai_tests:
needs: [init_environment]
if: contains(fromJSON('["openai", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-openai-tests.yml
with:
# Run expensive tests on weekly schedule (Sunday) or when manually requested
run_expensive_tests: ${{ (github.event_name == 'schedule' && github.event.schedule == '0 0 * * 0') || github.event.inputs.run_expensive_tests == 'true' }}
secrets: inherit
langchain_tests:
needs: [init_environment]
if: contains(fromJSON('["langchain", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-langchain-tests.yml
secrets: inherit
langchain_legacy_tests:
needs: [init_environment]
if: contains(fromJSON('["langchain_legacy", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-langchain-legacy-tests.yml
secrets: inherit
llama_index_tests:
needs: [init_environment]
if: contains(fromJSON('["llama_index", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-llama-index-tests.yml
secrets: inherit
anthropic_tests:
needs: [init_environment]
if: contains(fromJSON('["anthropic", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-anthropic-tests.yml
secrets: inherit
mistral_tests:
needs: [init_environment]
if: contains(fromJSON('["mistral", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-mistral-tests.yml
secrets: inherit
groq_tests:
needs: [init_environment]
if: contains(fromJSON('["groq", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-groq-tests.yml
secrets: inherit
aisuite_tests:
needs: [init_environment]
if: contains(fromJSON('["aisuite", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-aisuite-tests.yml
secrets: inherit
haystack_tests:
needs: [init_environment]
if: contains(fromJSON('["haystack", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-haystack-tests.yml
secrets: inherit
# guardrails-ai is on PyPI quarantine, so the install step fails before
# pytest runs. Re-enable once the package is back on PyPI.
# guardrails_tests:
# needs: [init_environment]
# if: contains(fromJSON('["guardrails", "all"]'), needs.init_environment.outputs.LIBS)
# uses: ./.github/workflows/lib-guardrails-tests.yml
# secrets: inherit
dspy_tests:
needs: [init_environment]
if: contains(fromJSON('["dspy", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-dspy-tests.yml
secrets: inherit
crewai_v0_tests:
needs: [init_environment]
if: contains(fromJSON('["crewai_v0", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-crewai-v0-tests.yml
secrets: inherit
crewai_v1_tests:
needs: [init_environment]
if: contains(fromJSON('["crewai_v1", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-crewai-v1-tests.yml
secrets: inherit
genai_tests:
needs: [init_environment]
if: contains(fromJSON('["genai", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-genai-tests.yml
with:
# Run expensive tests on weekly schedule (Sunday) or when manually requested
run_expensive_tests: ${{ (github.event_name == 'schedule' && github.event.schedule == '0 0 * * 0') || github.event.inputs.run_expensive_tests == 'true' }}
secrets: inherit
adk_tests:
needs: [init_environment]
if: contains(fromJSON('["adk", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-adk-tests.yml
secrets: inherit
adk_legacy_1_3_0_tests:
needs: [init_environment]
if: contains(fromJSON('["adk_legacy_1_3_0", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-adk-legacy-1-3-0-tests.yml
secrets: inherit
evaluation_metrics_tests:
needs: [init_environment]
if: contains(fromJSON('["metrics", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-metrics-with-llm-judge-tests.yml
secrets: inherit
bedrock_tests:
needs: [init_environment]
if: contains(fromJSON('["bedrock", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-bedrock-tests.yml
secrets: inherit
litellm_tests:
needs: [init_environment]
if: contains(fromJSON('["litellm", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-litellm-tests.yml
secrets: inherit
harbor_tests:
needs: [init_environment]
if: contains(fromJSON('["harbor", "all"]'), needs.init_environment.outputs.LIBS)
uses: ./.github/workflows/lib-harbor-tests.yml
secrets: inherit
# ========================================
# Slack Notification (manual and scheduled runs only)
# ========================================
notify-slack:
name: "Slack Notification"
runs-on: ubuntu-latest
needs:
- openai_tests
- langchain_tests
- langchain_legacy_tests
- llama_index_tests
- anthropic_tests
- mistral_tests
- groq_tests
- aisuite_tests
- haystack_tests
# - guardrails_tests # disabled: guardrails-ai is on PyPI quarantine
- dspy_tests
- crewai_v0_tests
- crewai_v1_tests
- genai_tests
- adk_tests
- adk_legacy_1_3_0_tests
- evaluation_metrics_tests
- bedrock_tests
- litellm_tests
- harbor_tests
if: always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
steps:
- name: Checkout
uses: actions/checkout@v6
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: false
- name: Determine trigger type
id: trigger
run: |
if [ "${{ github.event_name }}" == "schedule" ]; then
TYPE=$([[ "${{ github.event.schedule }}" == "0 0 * * 0" ]] && echo "Weekly Schedule" || echo "Daily Schedule")
else
TYPE="Manual Dispatch"
fi
echo "type=$TYPE" >> "$GITHUB_OUTPUT"
- name: Send Slack notification
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_USER_ID: ${{ secrets.SLACK_SDK_TESTS_NOTIFY_USER_ID }}
TRIGGER_TYPE: ${{ steps.trigger.outputs.type }}
SUITE_RESULTS: >-
{
"OpenAI": "${{ needs.openai_tests.result }}",
"LangChain": "${{ needs.langchain_tests.result }}",
"LangChain Legacy": "${{ needs.langchain_legacy_tests.result }}",
"LlamaIndex": "${{ needs.llama_index_tests.result }}",
"Anthropic": "${{ needs.anthropic_tests.result }}",
"Mistral": "${{ needs.mistral_tests.result }}",
"Groq": "${{ needs.groq_tests.result }}",
"AISuite": "${{ needs.aisuite_tests.result }}",
"Haystack": "${{ needs.haystack_tests.result }}",
"DSPy": "${{ needs.dspy_tests.result }}",
"CrewAI v0": "${{ needs.crewai_v0_tests.result }}",
"CrewAI v1": "${{ needs.crewai_v1_tests.result }}",
"GenAI": "${{ needs.genai_tests.result }}",
"ADK": "${{ needs.adk_tests.result }}",
"ADK Legacy 1.3.0": "${{ needs.adk_legacy_1_3_0_tests.result }}",
"Evaluation Metrics": "${{ needs.evaluation_metrics_tests.result }}",
"Bedrock": "${{ needs.bedrock_tests.result }}",
"LiteLLM": "${{ needs.litellm_tests.result }}",
"Harbor": "${{ needs.harbor_tests.result }}"
}
run: .github/scripts/notify-slack-lib-integration.sh
@@ -0,0 +1,76 @@
# Workflow to run Langchain tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib Langchain Tests for langchain < 1.0.0
run-name: "SDK Lib Langchain Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
GCP_CREDENTIALS_JSON: ${{ secrets.GCP_CREDENTIALS_JSON }}
GOOGLE_CLOUD_LOCATION: us-east1
GOOGLE_CLOUD_PROJECT: opik-sdk-tests
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
AWS_DEFAULT_REGION: us-east-1
AWS_ACCESS_KEY_ID: ${{ secrets.BEDROCK_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.BEDROCK_AWS_SECRET_ACCESS_KEY }}
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
on:
workflow_call:
jobs:
tests:
name: Langchain Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
matrix:
# 3.14+ is not supported by the legacy langchain (langchain<1.0.0)
python_version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/langchain/legacy_requirements.txt
- name: change aws role
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.BEDROCK_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.BEDROCK_AWS_SECRET_ACCESS_KEY }}
role-to-assume: ${{ vars.BEDROCK_ROLE }}
aws-region: us-east-1
role-chaining: true
role-skip-session-tagging: true
- name: Run tests
run: |
cd ./tests/library_integration/langchain/
python -m pytest -vv .
+75
View File
@@ -0,0 +1,75 @@
# Workflow to run Langchain tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib Langchain Tests
run-name: "SDK Lib Langchain Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
GCP_CREDENTIALS_JSON: ${{ secrets.GCP_CREDENTIALS_JSON }}
GOOGLE_CLOUD_LOCATION: us-east1
GOOGLE_CLOUD_PROJECT: opik-sdk-tests
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
AWS_DEFAULT_REGION: us-east-1
AWS_ACCESS_KEY_ID: ${{ secrets.BEDROCK_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.BEDROCK_AWS_SECRET_ACCESS_KEY }}
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
on:
workflow_call:
jobs:
tests:
name: Langchain Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
matrix:
python_version: ${{ fromJSON(vars.PYTHON_VERSIONS) }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/langchain/requirements.txt
- name: change aws role
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.BEDROCK_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.BEDROCK_AWS_SECRET_ACCESS_KEY }}
role-to-assume: ${{ vars.BEDROCK_ROLE }}
aws-region: us-east-1
role-chaining: true
role-skip-session-tagging: true
- name: Run tests
run: |
cd ./tests/library_integration/langchain/
python -m pytest -vv .
+61
View File
@@ -0,0 +1,61 @@
# Workflow to run LiteLLM integration tests
# This workflow is used to test the LiteLLM integration in opik.integrations.litellm, however
# there is another, external, integration with LiteLLM that lives in litellm repository, it's being tested
# in a separate workflow: .github/workflows/sdk-e2e-library-tests.yaml
#
# Please read inputs to provide correct values.
#
name: SDK Lib LiteLLM Tests
run-name: "SDK Lib LiteLLM Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
on:
workflow_call:
jobs:
tests:
name: LiteLLM Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
matrix:
python_version: ${{ fromJSON(vars.PYTHON_VERSIONS) }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/litellm/requirements.txt
- name: Run tests
run: |
cd ./tests/library_integration/litellm/
python -m pytest -vv .
@@ -0,0 +1,56 @@
# Workflow to run Langchain tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib LLama Index Tests
run-name: "SDK Lib LLama Index Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
on:
workflow_call:
jobs:
tests:
name: LLamaIndex Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
matrix:
python_version: ${{ fromJSON(vars.PYTHON_VERSIONS) }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/llama_index/requirements.txt
- name: Run tests
run: |
cd ./tests/library_integration/llama_index/
python -m pytest -vv .
@@ -0,0 +1,62 @@
# Workflow to run tests for evaluation metrics that use llm to judge scores
#
# Please read inputs to provide correct values.
#
name: SDK Lib OpenAI Tests
run-name: "SDK Lib Metrics Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
GCP_CREDENTIALS_JSON: ${{ secrets.GCP_CREDENTIALS_JSON }}
GOOGLE_CLOUD_LOCATION: us-east1
GOOGLE_CLOUD_PROJECT: opik-sdk-tests
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
on:
workflow_call:
jobs:
tests:
name: Evaluation Metrics Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
matrix:
python_version: ${{ fromJSON(vars.PYTHON_VERSIONS) }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/metrics_with_llm_judge/requirements.txt
pip list
- name: Run tests
run: |
cd ./tests/library_integration/metrics_with_llm_judge/
python -m pytest -vv --durations=20 .
+55
View File
@@ -0,0 +1,55 @@
# Workflow to run Mistral AI tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib Mistral Tests
run-name: "SDK Lib Mistral Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
on:
workflow_call:
jobs:
tests:
name: Mistral Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
matrix:
python_version: ["3.12"]
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/mistral/requirements.txt
- name: Run tests
run: |
cd ./tests/library_integration/mistral/
python -m pytest -vv .
+73
View File
@@ -0,0 +1,73 @@
# Workflow to run OpenAI tests
#
# Please read inputs to provide correct values.
#
name: SDK Lib OpenAI Tests
run-name: "SDK Lib OpenAI Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
GCP_CREDENTIALS_JSON: ${{ secrets.GCP_CREDENTIALS_JSON }}
GOOGLE_CLOUD_LOCATION: us-east1
GOOGLE_CLOUD_PROJECT: opik-sdk-tests
GOOGLE_GENAI_USE_VERTEXAI: TRUE
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
OPIK_TEST_EXPENSIVE: ${{ inputs.run_expensive_tests }}
on:
workflow_call:
inputs:
run_expensive_tests:
description: "Run expensive tests (e.g., video generation)"
required: false
type: boolean
default: false
jobs:
tests:
name: OpenAI Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: sdks/python
strategy:
fail-fast: true
matrix:
python_version: ${{ fromJSON(vars.PYTHON_VERSIONS) }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Install opik
run: pip install .
- name: Install test tools
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Install lib
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r library_integration/openai/requirements.txt
pip list
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_CREDENTIALS_JSON }}
- name: Run tests
run: |
cd ./tests/library_integration/openai/
python -m pytest -vv .
+161
View File
@@ -0,0 +1,161 @@
name: Lint Opik Helm Chart
run-name: "Lint Opik Helm Chart ${{ github.ref_name }} by @${{ github.actor }}"
on:
workflow_dispatch:
pull_request:
paths:
- "deployment/helm_chart/opik/**"
push:
branches:
- 'main'
paths:
- "deployment/helm_chart/opik/**"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# This workflow only reads the repo (lint + render); no write access needed.
permissions:
contents: read
jobs:
lint-helm-chart:
runs-on: ubuntu-latest
timeout-minutes: 5
strategy:
fail-fast: false
matrix:
# Prove dual compatibility: the chart must keep working on Helm 3.x
# while also rendering cleanly under Helm 4.x (DND-537).
helm-version:
- v3.21.0
- v4.2.0
name: lint-helm-chart (Helm ${{ matrix.helm-version }})
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Helm ${{ matrix.helm-version }}
uses: azure/setup-helm@v4.3.1
with:
version: ${{ matrix.helm-version }}
- name: Run lint on Helm chart
run: |
set -e
cd deployment/helm_chart/opik
helm repo add mysql https://comet-ml.github.io/comet-mysql-helm/
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add clickhouse-operator https://docs.altinity.com/clickhouse-operator
helm dependency build
helm lint --values values.yaml .
cd -
# Assert the rendered content of specific resources (e.g. the `opik.probe`
# helper) via helm-unittest. Tests live in deployment/helm_chart/opik/tests/.
unittest-helm-chart:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Helm
uses: azure/setup-helm@v4.3.1
with:
version: v3.21.0
- name: Install helm-unittest plugin
run: helm plugin install https://github.com/helm-unittest/helm-unittest
- name: Run helm unittest
run: |
set -e
cd deployment/helm_chart/opik
helm repo add mysql https://comet-ml.github.io/comet-mysql-helm/
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add clickhouse-operator https://docs.altinity.com/clickhouse-operator
helm dependency build
helm unittest .
cd -
# Prove the chart renders equivalently under Helm 3.x and Helm 4.x so we can ship
# Helm 4 support without changing behavior for Helm 3 users (DND-537). Each version
# resolves its own dependencies (`helm dependency build`) and renders from scratch.
# Helm 4 only differs from Helm 3 in trailing whitespace it emits between manifests,
# which YAML ignores and does not change any rendered Kubernetes object — so we
# compare after stripping trailing whitespace and blank lines.
render-equality:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Render with Helm 3.x
uses: azure/setup-helm@v4.3.1
with:
version: v3.21.0
- name: helm template (Helm 3.x)
run: |
set -e
cd deployment/helm_chart/opik
rm -rf charts Chart.lock
helm repo add mysql https://comet-ml.github.io/comet-mysql-helm/
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add clickhouse-operator https://docs.altinity.com/clickhouse-operator
helm dependency build
helm template opik . --values values.yaml > "${RUNNER_TEMP}/render-helm3.yaml"
cd -
- name: Render with Helm 4.x
uses: azure/setup-helm@v4.3.1
with:
version: v4.2.0
- name: helm template (Helm 4.x)
run: |
set -e
cd deployment/helm_chart/opik
# Wipe Helm 3's resolved deps so Helm 4 runs its own dependency resolution.
rm -rf charts Chart.lock
helm repo add mysql https://comet-ml.github.io/comet-mysql-helm/
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add clickhouse-operator https://docs.altinity.com/clickhouse-operator
helm dependency build
helm template opik . --values values.yaml > "${RUNNER_TEMP}/render-helm4.yaml"
cd -
- name: Diff rendered output (semantically; must be identical)
run: |
set -e
# Compare by parsing every rendered YAML document and re-serializing it
# canonically (sorted keys), instead of a line-based diff. This ignores the
# ONLY thing Helm 4 changes vs Helm 3 — cosmetic inter-document spacing and
# trailing whitespace — while preserving real content, including blank lines
# *inside* block scalars (e.g. embedded config in ConfigMaps). A line-based
# blank-line strip would hide those; a YAML round-trip does not.
canonicalize() {
python3 - "$1" <<'PY'
import sys, yaml
docs = [d for d in yaml.safe_load_all(open(sys.argv[1])) if d is not None]
sys.stdout.write(yaml.safe_dump_all(docs, sort_keys=True, default_flow_style=False))
PY
}
canonicalize "${RUNNER_TEMP}/render-helm3.yaml" > "${RUNNER_TEMP}/render-helm3.norm.yaml"
canonicalize "${RUNNER_TEMP}/render-helm4.yaml" > "${RUNNER_TEMP}/render-helm4.norm.yaml"
if ! diff -u "${RUNNER_TEMP}/render-helm3.norm.yaml" "${RUNNER_TEMP}/render-helm4.norm.yaml"; then
echo "::error::Rendered manifests differ between Helm 3.x and Helm 4.x. The chart must render equivalently on both."
exit 1
fi
echo "Helm 3.x and Helm 4.x produce equivalent rendered manifests."
+239
View File
@@ -0,0 +1,239 @@
name: Load Tests (SDK ingestion)
run-name: "Load Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
# checks: write so EnricoMi/publish-unit-test-result-action can post the
# "Load Test Results" check; without it the step gets 403 Forbidden and
# fails the whole workflow even when all scenarios passed.
checks: write
on:
schedule:
- cron: '0 4 * * 0' # Weekly, Sunday 04:00 UTC
workflow_dispatch:
env:
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
OPIK_URL_OVERRIDE: http://localhost:8080
OPIK_CONSOLE_LOGGING_LEVEL: WARNING
jobs:
run-load-tests:
# Larger GitHub-hosted runner (~4 vCPU / 16 GB) instead of the default
# ubuntu-latest (2 vCPU / 7 GB). The heaviest ingestion scenarios
# (e.g. test_many_spans_per_trace) intermittently OOM-killed an xdist
# worker on 7 GB; the extra memory headroom is what stops that.
runs-on: ubuntu-latest-m
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Run latest Opik server
env:
OPIK_USAGE_REPORT_ENABLED: false
COMPOSE_BAKE: false
TOGGLE_RUNNERS_ENABLED: "true"
run: |
cd ${{ github.workspace }}
./opik.sh --backend --port-mapping --build
- name: Check Opik server availability
shell: bash
run: |
chmod +x ${{ github.workspace }}/tests_end_to_end/installer_utils/*.sh
cd ${{ github.workspace }}/deployment/docker-compose
${{ github.workspace }}/tests_end_to_end/installer_utils/check_docker_compose_pods.sh
${{ github.workspace }}/tests_end_to_end/installer_utils/check_backend.sh
- name: Install Opik SDK
run: |
cd ${{ github.workspace }}/sdks/python
pip install .
- name: Install Python SDK load-test requirements
run: |
cd ${{ github.workspace }}/tests_load/suite/python_sdk
pip install -r requirements.txt
- name: Run Python SDK load tests
# -n 2 + --dist=worksteal runs two scenarios in parallel at a time.
# History: on the old 7 GB ubuntu-latest, `-n auto` (= 4 workers)
# reliably OOM-killed the heaviest scenarios
# (test_many_traces_one_span_each, test_many_spans_per_trace) when
# co-scheduled against the same docker-compose Opik stack, and even
# -n 2 still crashed a worker occasionally. The job now runs on the
# larger ubuntu-latest-m (~16 GB, see runs-on above), so -n 2 has
# comfortable memory headroom; it's kept conservative for now and can
# be raised toward -n auto on the larger runner if more parallelism is
# wanted. Each test uses a unique project so isolation holds. Worksteal
# balances the very uneven per-test durations (spread is window-locked
# at 600 s, others run in 1-6 min).
run: |
cd ${{ github.workspace }}/tests_load
pytest suite/python_sdk -n 2 --dist=worksteal --junitxml=${{ github.workspace }}/load_test_results.xml
- name: Render metrics into job summary
if: always()
run: |
python - <<'PY' >> "$GITHUB_STEP_SUMMARY"
import json
import pathlib
metrics_dir = pathlib.Path("tests_load/.last_run")
files = sorted(metrics_dir.glob("*.json")) if metrics_dir.exists() else []
if not files:
print("## Load test metrics\n\n_No metrics produced (suite did not run)._")
raise SystemExit
def fmt_seconds(value):
if not isinstance(value, (int, float)):
return "—"
return f"{value / 60:.1f} m" if value >= 60 else f"{value:.1f} s"
def fmt_count(value):
return f"{value:,}" if isinstance(value, (int, float)) else "—"
def submit_seconds(metrics):
# The submit phase is timed as "logging" for trace-based tests and
# as "insert" for the dataset-versions test.
for key in ("logging_seconds", "insert_seconds"):
if isinstance(metrics.get(key), (int, float)):
return metrics[key]
return None
def submitted_volume(metrics):
# Returns (count, label). Picks the most representative unit per
# test type so the table shows what was actually submitted.
if "expected_total_items" in metrics:
return metrics["expected_total_items"], (
f"{metrics['expected_total_items']:,} items"
)
if "total_traces" in metrics:
return metrics["total_traces"], (
f"{metrics['total_traces']:,} traces"
)
trace_count = metrics.get("trace_count")
spans_per_trace = metrics.get("spans_per_trace")
if isinstance(trace_count, int) and isinstance(spans_per_trace, int):
return trace_count, (
f"{trace_count:,} traces × {spans_per_trace} spans"
)
if isinstance(trace_count, int):
return trace_count, f"{trace_count:,} traces"
return None, "—"
def submit_rate(count, seconds):
if count is None or not isinstance(seconds, (int, float)) or seconds <= 0:
return "—"
return f"{count / seconds:,.0f}/s"
def total_seconds(metrics):
parts = [
metrics.get(key)
for key in (
"logging_seconds",
"insert_seconds",
"flush_seconds",
"verify_seconds",
)
]
parts = [p for p in parts if isinstance(p, (int, float))]
return sum(parts) if parts else None
def delivered_count(metrics):
for key in ("delivered_trace_count", "delivered_item_count"):
if key in metrics:
return metrics[key]
return None
print("## Load test metrics\n")
print(
"_Submit = time spent calling decorated functions / context managers."
" Submit rate = volume ÷ submit time."
" Total = submit + flush + verify._\n"
)
print(
"| Test | Volume | Submit | Submit rate"
" | Flush | Verify | Total | Delivered |"
)
print(
"| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |"
)
payloads = []
for path in files:
metrics = json.loads(path.read_text())
payloads.append(metrics)
name = metrics.get("test_name", path.stem)
count, volume_label = submitted_volume(metrics)
submit_s = submit_seconds(metrics)
print(
f"| `{name}`"
f" | {volume_label}"
f" | {fmt_seconds(submit_s)}"
f" | {submit_rate(count, submit_s)}"
f" | {fmt_seconds(metrics.get('flush_seconds'))}"
f" | {fmt_seconds(metrics.get('verify_seconds'))}"
f" | {fmt_seconds(total_seconds(metrics))}"
f" | {fmt_count(delivered_count(metrics))} |"
)
print("\n<details>\n<summary>Per-test metrics (raw JSON)</summary>\n")
for metrics in payloads:
print(f"\n**{metrics.get('test_name', '?')}**")
print("```json")
print(json.dumps(metrics, indent=2))
print("```")
print("\n</details>")
PY
- name: Upload metrics report
if: always()
uses: actions/upload-artifact@v7
with:
name: load-test-metrics
path: ${{ github.workspace }}/tests_load/.last_run/
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action/linux@v2
if: always()
with:
action_fail: true
comment_mode: failures
check_name: Load Test Results
files: ${{ github.workspace }}/load_test_results.xml
- name: Keep BE log in case of failure
if: failure()
run: |
docker logs opik-backend-1 > ${{ github.workspace }}/opik-backend.log
- name: Attach BE log
if: failure()
uses: actions/upload-artifact@v7
with:
name: opik-backend-log
path: ${{ github.workspace }}/opik-backend.log
- name: Stop opik server
if: always()
run: |
cd ${{ github.workspace }}
./opik.sh --stop
@@ -0,0 +1,31 @@
name: "Database Migration Prefix Check"
run-name: "Backend Database Migration Prefix Check on ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
on:
pull_request:
paths:
- "apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/**"
- "apps/opik-backend/src/main/resources/liquibase/db-app-analytics/migrations/**"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
check-migration-prefix-conflicts:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout PR branch
uses: actions/checkout@v6
with:
fetch-depth: 0 # Fetch full history to compare with main
- name: Check for migration prefix conflicts
shell: bash
run: ./scripts/check_backend_migration_conflicts.sh
@@ -0,0 +1,195 @@
name: "Opik Optimizer - E2E Tests"
run-name: "Opik Optimizer E2E Tests ${{ github.ref_name }} by @${{ github.actor }}"
concurrency:
group: opik-optimizer-e2e-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
pull_request:
paths:
- 'sdks/opik_optimizer/**'
- '.github/workflows/opik-optimizer-e2e-tests.yaml'
push:
branches:
- 'main'
paths:
- 'sdks/opik_optimizer/**'
- '.github/workflows/opik-optimizer-e2e-tests.yaml'
permissions:
contents: read
packages: write
env:
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
HF_HUB_ENABLE_PROGRESS_BARS: "0"
HF_HUB_DISABLE_TELEMETRY: "1"
OPIK_URL_OVERRIDE: http://localhost:8080
OPIK_CONSOLE_LOGGING_LEVEL: DEBUG
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
jobs:
build-opik:
uses: ./.github/workflows/build_e2e_docker.yaml
with:
include_guardrails: true
run-e2e-tests:
name: Opik Optimizer E2E Tests Python ${{matrix.python_version}}
needs: build-opik
runs-on: ubuntu-latest
timeout-minutes: 40
defaults:
run:
working-directory: sdks/opik_optimizer/
strategy:
fail-fast: false
matrix:
python_version: ${{ fromJSON(vars.PYTHON_VERSIONS) }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull Docker images from GHCR
env:
TAG: ${{ needs.build-opik.outputs.image_tag }}
run: |
docker pull "ghcr.io/comet-ml/opik/opik-backend:$TAG"
docker pull "ghcr.io/comet-ml/opik/opik-python-backend:$TAG"
docker pull "ghcr.io/comet-ml/opik/opik-guardrails-backend:$TAG"
docker tag "ghcr.io/comet-ml/opik/opik-backend:$TAG" ghcr.io/comet-ml/opik/opik-backend:latest
docker tag "ghcr.io/comet-ml/opik/opik-python-backend:$TAG" ghcr.io/comet-ml/opik/opik-python-backend:latest
docker tag "ghcr.io/comet-ml/opik/opik-guardrails-backend:$TAG" ghcr.io/comet-ml/opik/opik-guardrails-backend:latest
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
cache: 'pip'
cache-dependency-path: |
sdks/opik_optimizer/pyproject.toml
sdks/opik_optimizer/setup.py
sdks/opik_optimizer/tests/test_requirements.txt
- name: Run latest Opik server
env:
OPIK_USAGE_REPORT_ENABLED: false
# Avoid Buildx Bake concurrent image export collisions in CI.
COMPOSE_BAKE: false
TOGGLE_RUNNERS_ENABLED: "true"
OPIK_BACKEND_PULL_POLICY: never
PYTHON_BACKEND_PULL_POLICY: never
OPIK_GUARDRAILS_BACKEND_PULL_POLICY: never
run: |
cd ${{ github.workspace }}
./opik.sh --backend --port-mapping --guardrails
- name: Check Opik server availability
shell: bash
run: |
chmod +x ${{ github.workspace }}/tests_end_to_end/installer_utils/*.sh
cd ${{ github.workspace }}/deployment/docker-compose
echo "Check Docker pods are up"
${{ github.workspace }}/tests_end_to_end/installer_utils/check_docker_compose_pods.sh
echo "Check backend health"
${{ github.workspace }}/tests_end_to_end/installer_utils/check_backend.sh
- name: Cache HuggingFace datasets
uses: actions/cache@v4
with:
path: ~/.cache/huggingface
key: hf-datasets-${{ hashFiles('**/datasets/*.py') }}
restore-keys: |
hf-datasets-
- name: Cache LiteLLM cache
uses: actions/cache@v4
with:
path: ~/.litellm_cache
key: litellm-cache-${{ hashFiles('**/requirements.txt', '**/pyproject.toml', '**/setup.py') }}
restore-keys: |
litellm-cache-
- name: Install opik_optimizer
run: pip install .[dev]
- name: Install test requirements
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Run E2E tests
run: pytest tests/e2e -vv --junitxml=${{ github.workspace }}/test_results_${{matrix.python_version}}.xml
- name: Upload test artifacts on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: opik-optimizer-e2e-logs-p${{matrix.python_version}}
path: |
*.log
tests/**/*.log
retention-days: 7
- name: Stop opik server
if: always()
run: |
cd ${{ github.workspace }}
./opik.sh --stop --guardrails
dataset-source-check:
name: Opik Optimizer Integration Smoke Tests
runs-on: ubuntu-latest
timeout-minutes: 30
needs: run-e2e-tests
env:
OPIK_USE_HF_STREAMING: "true"
defaults:
run:
working-directory: sdks/opik_optimizer/
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: 'pip'
cache-dependency-path: |
sdks/opik_optimizer/pyproject.toml
sdks/opik_optimizer/setup.py
sdks/opik_optimizer/tests/test_requirements.txt
- name: Cache HuggingFace datasets
uses: actions/cache@v4
with:
path: ~/.cache/huggingface
key: hf-datasets-${{ hashFiles('**/datasets/*.py') }}
restore-keys: |
hf-datasets-
- name: Install opik_optimizer
run: pip install .
- name: Install test requirements
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Run dataset integration check
run: pytest -n auto --dist loadfile tests/integration/datasets/test_dataset_sources.py -vv
@@ -0,0 +1,46 @@
name: "Opik Optimizer - Publish SDK"
run-name: "Opik Optimizer Publish SDK ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
packages: write
on:
workflow_dispatch:
jobs:
build-and-publish:
runs-on: ubuntu-latest
timeout-minutes: 5
env:
HF_HUB_ENABLE_PROGRESS_BARS: "0"
HF_HUB_DISABLE_TELEMETRY: "1"
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
python-version: 3.9
- name: PyPI token preflight
uses: ./.github/actions/pypi-token-preflight
with:
pypi_token: ${{ secrets.PYPI_API_TOKEN }}
package_name: opik-optimizer
- name: Build pip package
run: |
cd sdks/opik_optimizer
pip3 install -U pip build
python3 -m build --sdist --wheel --outdir dist/ .
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@v1.12.4
with:
password: ${{ secrets.PYPI_API_TOKEN }}
packages-dir: sdks/opik_optimizer/dist
# Symmetric to OPIK-6624's npm publish 403-skip: replays / races where
# the version is already on PyPI should not fail the release.
skip-existing: true
@@ -0,0 +1,82 @@
name: "Opik Optimizer - Unit Tests"
run-name: "Opik Optimizer Unit Tests ${{ github.ref_name }} by @${{ github.actor }}"
on:
workflow_dispatch:
pull_request:
paths:
- 'sdks/opik_optimizer/**'
push:
branches:
- 'main'
paths:
- 'sdks/opik_optimizer/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
HF_HUB_OFFLINE: true
HF_DATASETS_OFFLINE: true
HF_HUB_ENABLE_PROGRESS_BARS: "0"
HF_HUB_DISABLE_TELEMETRY: "1"
permissions:
contents: read
jobs:
run-unit-tests:
name: Opik Optimizer Unit Tests Python ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: sdks/opik_optimizer/
strategy:
fail-fast: false
matrix:
python_version: ${{ fromJSON(vars.PYTHON_VERSIONS || '["3.10","3.11","3.12","3.13","3.14"]') }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
cache: 'pip'
cache-dependency-path: |
sdks/opik_optimizer/pyproject.toml
sdks/opik_optimizer/setup.py
sdks/opik_optimizer/tests/test_requirements.txt
- name: Cache HuggingFace datasets
uses: actions/cache@v4
with:
path: ~/.cache/huggingface
key: hf-datasets-${{ hashFiles('**/datasets/*.py') }}
restore-keys: |
hf-datasets-
- name: Cache LiteLLM cache
uses: actions/cache@v4
with:
path: ~/.litellm_cache
key: litellm-cache-${{ hashFiles('**/requirements.txt', '**/pyproject.toml', '**/setup.py') }}
restore-keys: |
litellm-cache-
- name: Install opik_optimizer
run: pip install .
- name: Install test requirements
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt
- name: Run unit tests
run: pytest -n auto --dist loadfile tests/unit -vv --junitxml=${{ github.workspace }}/test_results_${{matrix.python_version}}.xml
+163
View File
@@ -0,0 +1,163 @@
name: Opik TypeScript Configure Publish
run-name: "Opik TypeScript Configure Publish from ${{github.ref_name}} by @${{ github.actor }}"
permissions:
contents: write
packages: write
on:
workflow_dispatch:
inputs:
version:
type: string
required: true
description: Version
default: ""
is_release:
type: boolean
required: false
default: false
workflow_call:
inputs:
version:
type: string
required: true
description: Version
is_release:
type: boolean
required: false
default: false
jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
VERSION: ${{ github.event.inputs.version || inputs.version }}
IS_RELEASE: ${{ github.event.inputs.is_release || inputs.is_release }}
defaults:
run:
working-directory: sdks/typescript/src/opik/configure
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
fetch-depth: 0
token: ${{ secrets.GH_PAT_TO_ACCESS_GITHUB_API }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"
- name: Resolve published package name
if: ${{ env.IS_RELEASE }}
id: resolve_pkg_name
shell: bash
working-directory: sdks/typescript/src/opik/configure
run: |
# Derive from package.json so the preflight always probes the actual publish
# target, even if the package is renamed (e.g., opik-configure -> opik-ts).
PKG=$(node -p "require('./package.json').name")
echo "name=${PKG}" >> "$GITHUB_OUTPUT"
- name: NPM token preflight
if: ${{ env.IS_RELEASE }}
uses: ./.github/actions/npm-token-preflight
with:
npm_token: ${{ secrets.NPM_TOKEN }}
package_name: ${{ steps.resolve_pkg_name.outputs.name }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.5
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Validate version format
if: ${{ env.IS_RELEASE }}
run: |
if ! [[ "${{ env.VERSION }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "Error: Invalid version format '${{ env.VERSION }}'. Expected format: X.Y.Z or X.Y.Z-suffix"
exit 1
fi
- name: Update version
if: ${{ env.IS_RELEASE }}
run: pnpm version ${{ env.VERSION }} --no-git-tag-version
- name: Lint and format check
run: |
pnpm run fmt:check || echo "Warning: Format check failed"
pnpm run lint || echo "Warning: Lint check failed"
- name: Build package
run: pnpm build
- name: Verify build
run: |
if [ ! -d "dist" ]; then
echo "Error: Build failed - dist directory not found"
exit 1
fi
- name: Check if version already exists on NPM
if: ${{ env.IS_RELEASE }}
run: |
PACKAGE_NAME=$(node -p "require('./package.json').name")
if npm view "$PACKAGE_NAME@${{ env.VERSION }}" version 2>/dev/null; then
echo "Error: Version ${{ env.VERSION }} already exists on NPM"
exit 1
fi
echo "Version check passed - ${{ env.VERSION }} is available"
- name: Commit version changes
if: ${{ env.IS_RELEASE }}
run: |
git config --local user.email "github-actions@comet.com"
git config --local user.name "github-actions"
git add package.json pnpm-lock.yaml
git diff --staged --quiet || git commit -m "chore: update Opik Configure version to ${{ env.VERSION }}"
- name: Create git tag
if: ${{ env.IS_RELEASE }}
run: |
git tag -a "configure-v${{ env.VERSION }}" -m "Release Opik Configure v${{ env.VERSION }}"
- name: Publish to NPM
if: ${{ env.IS_RELEASE }}
run: |
echo "Publishing opik-configure version ${{ env.VERSION }} to NPM..."
pnpm publish --no-git-checks --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Push changes and tags
if: ${{ env.IS_RELEASE }}
run: |
git push origin ${{ github.ref }}
git push origin "configure-v${{ env.VERSION }}"
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_TO_ACCESS_GITHUB_API }}
- name: Workflow summary
if: always()
run: |
exec >> "$GITHUB_STEP_SUMMARY"
echo "## Workflow Summary"
echo ""
echo "- **Version**: ${{ env.VERSION }}"
echo "- **Is Release**: ${{ env.IS_RELEASE }}"
echo "- **Branch**: ${{ github.ref_name }}"
echo "- **Triggered by**: @${{ github.actor }}"
echo ""
if [ "${{ env.IS_RELEASE }}" == "true" ]; then
echo "✅ Package published to NPM: [opik-configure@${{ env.VERSION }}](https://www.npmjs.com/package/opik-configure/v/${{ env.VERSION }})"
echo "✅ Git tag created: configure-v${{ env.VERSION }}"
else
echo "️ Dry run completed - no changes published"
fi
+77
View File
@@ -0,0 +1,77 @@
name: 🏷️ Auto Assign PR
on:
workflow_call:
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
assign_pr_creator_or_pusher:
name: Assign PR Creator or Pusher
runs-on: ubuntu-latest
timeout-minutes: 1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
pull-requests: write
steps:
- name: Determine user to assign
id: get_assignee_user
run: |
ASSIGN_USER=""
EVENT_ACTOR=""
if [[ "${{ github.event.action }}" == "synchronize" ]]; then
EVENT_ACTOR="${{ github.event.sender.login }}"
else
EVENT_ACTOR="${{ github.event.pull_request.user.login }}"
fi
KNOWN_BOTS=("dependabot[bot]" "github-actions[bot]" "github-copilot[bot]" "copilot-bot[bot]")
IS_KNOWN_BOT=false
for BOT in "${KNOWN_BOTS[@]}"; do
if [[ "$EVENT_ACTOR" == "$BOT" ]]; then
IS_KNOWN_BOT=true
break
fi
done
if $IS_KNOWN_BOT || [[ "$EVENT_ACTOR" =~ \[bot\]$ ]]; then
echo "Skipping bot user: $EVENT_ACTOR"
ASSIGN_USER=""
else
ASSIGN_USER="$EVENT_ACTOR"
echo "Determined user: $ASSIGN_USER"
fi
echo "assigned_user=$ASSIGN_USER" >> "$GITHUB_OUTPUT"
- name: Assign PR to determined user
if: steps.get_assignee_user.outputs.assigned_user != ''
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
ASSIGN_USER_LOGIN="${{ steps.get_assignee_user.outputs.assigned_user }}"
echo "Attempting to assign PR #$PR_NUMBER to $ASSIGN_USER_LOGIN"
# Notes:
# - If the user is already assigned, `gh pr edit` succeeds without error.
# - External-collaborator PRs may fail outright (token lacks write on that user) — we
# log and continue rather than failing the workflow.
# - GitHub can also silently ignore assignees without push access while still exiting 0,
# so we re-read the PR's assignees after the edit and confirm the user actually landed
# before logging success.
if gh pr edit "$PR_NUMBER" --add-assignee "$ASSIGN_USER_LOGIN" --repo "${{ github.repository }}"; then
CURRENT_ASSIGNEES="$(gh pr view "$PR_NUMBER" --repo "${{ github.repository }}" --json assignees --jq '.assignees[].login' 2>/dev/null || true)"
if grep -Fxq "$ASSIGN_USER_LOGIN" <<<"$CURRENT_ASSIGNEES"; then
echo "Successfully assigned PR #$PR_NUMBER to $ASSIGN_USER_LOGIN"
else
echo "⚠ gh pr edit returned success but $ASSIGN_USER_LOGIN is not in the assignees list (likely silently ignored by GitHub — no push access)"
fi
else
echo "❌ Failed to assign PR #$PR_NUMBER to $ASSIGN_USER_LOGIN, likely an external collaborator"
fi
+115
View File
@@ -0,0 +1,115 @@
permissions:
contents: read
pull-requests: write
name: PR Linter
# This workflow runs on pull requests that are opened, edited, or have new commits pushed.
on:
pull_request:
types: [opened, edited, synchronize]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
lint-pr:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: Validate PR Title, Description, and Commits
uses: actions/github-script@v7
with:
script: |
// Get the PR context
const pr = context.payload.pull_request;
if (!pr) {
core.setFailed("Could not get PR from context.");
return;
}
const owner = context.repo.owner;
const repo = context.repo.repo;
let errors = [];
// 1. Validate PR Title
const title = pr.title;
const titleRegex = /^\[(OPIK-\d+|DND-\d+|DEV-\d+|CUST-\d+|issue-\d+|NA)\](\s*\[(BE|FE|DOCS|SDK|GHA|CI|HELM)\])*\s*.+$/;
if (!titleRegex.test(title)) {
errors.push(
"❌ **Invalid Title Format.** Your PR title must include a ticket/issue number and may optionally include component tags (`[FE]`, `[BE]`, etc.).\n\n" +
" - **Internal contributors: Open a JIRA ticket and link to it:** `[OPIK-xxxx]` or `[CUST-xxxx]` or `[DND-xxxx]` or `[DEV-xxxx] [COMPONENT] Your change`\n" +
" - **External contributors: Open a Github Issue and link to it via its number:** `[issue-xxxx] [COMPONENT] Your change`\n" +
" - **No ticket:** Use `[NA] [COMPONENT] Your change` (Issues section not required)\n\n" +
" *Example: `[issue-3108] [BE] [FE] Fix authentication bug` or `[OPIK-1234] Fix bug` or `[NA] Update README`*"
);
}
// 2. Validate PR Description
const body = pr.body || "";
const requiredHeadings = ["## Details", "## Change checklist", "## Issues", "## Testing", "## Documentation"];
if (body.length === 0) {
errors.push("❌ **Missing Description.** The PR description cannot be empty.");
} else {
for (const heading of requiredHeadings) {
if (!body.includes(heading)) {
errors.push(`❌ **Missing Section.** The description is missing the \`${heading}\` section.`);
}
}
// Helper function to get content of a markdown section
const getSectionContent = (sectionTitle) => {
const startIndex = body.indexOf(sectionTitle);
if (startIndex === -1) return null;
const nextHeaderIndex = body.indexOf("## ", startIndex + sectionTitle.length);
const endIndex = nextHeaderIndex !== -1 ? nextHeaderIndex : body.length;
return body.substring(startIndex + sectionTitle.length, endIndex).trim();
};
const detailsContent = getSectionContent("## Details");
if (detailsContent !== null && detailsContent.length === 0) {
errors.push("❌ **Incomplete Details Section.** The `## Details` section cannot be empty.");
}
const issuesContent = getSectionContent("## Issues");
const issuesRegex = /(#\d+|OPIK-\d+|DND-\d+|DEV-\d+|CUST-\d+)/;
const isNATicket = title.startsWith("[NA]");
if (issuesContent !== null && !isNATicket && !issuesRegex.test(issuesContent)) {
errors.push(
"❌ **Incomplete Issues Section.** You must reference at least one GitHub issue (`#xxxx`), Jira ticket (`OPIK-xxxx`), CUST ticket (`CUST-xxxx`), DEV ticket (`DEV-xxxx`), or DND ticket (`DND-xxxx`) under the `## Issues` section."
);
}
}
// 3. Find all existing linter comments across all pages
const COMMENT_MARKER = "<!-- pr-linter-comment -->";
const allComments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr.number });
const markerComments = allComments.filter(c => c.body && c.body.includes(COMMENT_MARKER));
// 4. Final step: upsert/delete comment and/or fail the job
if (errors.length > 0) {
const commentBody = COMMENT_MARKER + "\n### 📋 PR Linter Failed\n\n" + errors.join("\n\n---\n\n");
const logMessage = "PR Linter Failed:\n\n" + errors.join("\n\n")
.replace(/❌ \*\*/g, '❌ ')
.replace(/\*\*`/g, '`')
.replace(/`\*\*/g, '`')
.replace(/###\s/g, '')
.replace(/---\n\n/g, '');
if (markerComments.length > 0) {
// Keep the newest, delete any duplicates
const [keep, ...duplicates] = [...markerComments].sort((a, b) => b.id - a.id);
await github.rest.issues.updateComment({ owner, repo, comment_id: keep.id, body: commentBody });
await Promise.all(duplicates.map(c => github.rest.issues.deleteComment({ owner, repo, comment_id: c.id })));
} else {
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: commentBody });
}
core.setFailed(logMessage);
} else {
await Promise.all(markerComments.map(c => github.rest.issues.deleteComment({ owner, repo, comment_id: c.id })));
console.log("✅ PR linting passed!");
}
+141
View File
@@ -0,0 +1,141 @@
name: 📏 Auto Label PR Size
on:
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
size-labeler:
name: Label PR by size
runs-on: ubuntu-latest
timeout-minutes: 2
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Compute size and apply label
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const pr = context.payload.pull_request;
const prNumber = pr.number;
// Uniform charcoal chip for every size label so the size metadata reads as one
// recessive family and doesn't compete with semantic labels (bug, Frontend, …) on
// a busy PR list. The cool -> hot ramp lives entirely in the per-bucket emoji
// (🔵 smallest -> 🔴 largest), which keeps its crisp shape on a neutral background.
// maxSize is the inclusive upper bound of changed LOC (additions + deletions) for
// the bucket; the last entry (null) catches everything above it.
const CHIP_COLOR = "2d3139";
const BUCKETS = [
{ name: "🔵 size/XS", color: CHIP_COLOR, maxSize: 19 },
{ name: "🟢 size/S", color: CHIP_COLOR, maxSize: 100 },
{ name: "🟡 size/M", color: CHIP_COLOR, maxSize: 300 },
{ name: "🟠 size/L", color: CHIP_COLOR, maxSize: 600 },
{ name: "🔴 size/XL", color: CHIP_COLOR, maxSize: null },
];
const MANAGED_LABELS = BUCKETS.map((b) => b.name);
// Lockfiles and generated clients dwarf the human-reviewed diff, so exclude them
// from the count — otherwise a lockfile bump mislabels a tiny PR as XL.
const IGNORE_GLOBS = [
"**/package-lock.json",
"**/yarn.lock",
"**/pnpm-lock.yaml",
"**/poetry.lock",
"**/uv.lock",
"sdks/python/src/opik/rest_api/**",
"sdks/typescript/src/opik/rest_api/**",
"**/*.snap",
"**/*.svg",
"**/*.png",
];
const globToRegExp = (glob) => {
let re = "";
for (let i = 0; i < glob.length; i++) {
if (glob.startsWith("**/", i)) { re += "(?:[^/]+/)*"; i += 2; continue; }
if (glob.startsWith("**", i)) { re += ".*"; i += 1; continue; }
const c = glob[i];
if (c === "*") { re += "[^/]*"; continue; }
if (".+^${}()|[]\\".includes(c)) { re += "\\" + c; continue; }
re += c;
}
return new RegExp("^" + re + "$");
};
const ignoreRes = IGNORE_GLOBS.map(globToRegExp);
const isIgnored = (path) => ignoreRes.some((re) => re.test(path));
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
const total = files
.filter((f) => !isIgnored(f.filename))
.reduce((sum, f) => sum + f.additions + f.deletions, 0);
const bucket =
BUCKETS.find((b) => b.maxSize !== null && total <= b.maxSize) ||
BUCKETS[BUCKETS.length - 1];
core.info(`PR #${prNumber}: ${total} changed LOC (excluding ignored files) -> ${bucket.name}`);
// Keep the label's color/emoji authoritative regardless of how it was first created.
try {
const { data: existing } = await github.rest.issues.getLabel({
owner,
repo,
name: bucket.name,
});
if (existing.color !== bucket.color) {
await github.rest.issues.updateLabel({
owner,
repo,
name: bucket.name,
color: bucket.color,
});
}
} catch (err) {
if (err.status !== 404) throw err;
await github.rest.issues.createLabel({
owner,
repo,
name: bucket.name,
color: bucket.color,
});
}
const current = pr.labels.map((l) => l.name);
for (const stale of current.filter(
(name) => MANAGED_LABELS.includes(name) && name !== bucket.name
)) {
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: stale,
});
} catch (err) {
if (err.status !== 404) throw err;
}
}
if (!current.includes(bucket.name)) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: [bucket.name],
});
}
@@ -0,0 +1,42 @@
name: Publish Cursor Extension
on:
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
defaults:
run:
working-directory: extensions/cursor
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install dependencies
run: npm ci
- name: Package Extension
run: npm run build
- name: Get Extension Filename
id: get_filename
run: echo "filename=$(ls ./*.vsix)" >> "$GITHUB_OUTPUT"
# TODO(OPIK-6617): Add an OVSX_PAT preflight before this step so a bad / expired
# Open VSX token fails fast with an actionable message instead of erroring out
# inside `ovsx publish`. See .github/actions/npm-token-preflight for the pattern.
- name: Publish Extension to Open VSX
env:
OVSX_PAT: ${{ secrets.OVSX_PAT }}
run: |
npx ovsx publish ./${{ steps.get_filename.outputs.filename }} -p ${{ secrets.OVSX_PAT }}
+161
View File
@@ -0,0 +1,161 @@
name: Publish Opik Helm Chart
run-name: "Publish Opik Helm Chart ${{ github.ref_name }} by @${{ github.actor }}"
on:
workflow_dispatch:
inputs:
version:
type: string
required: true
description: Version
default: ""
workflow_call:
inputs:
version:
type: string
required: true
description: Version
jobs:
publish-helm-chart:
# TODO(OPIK-6617): Add a preflight check that verifies the runner can push to the
# `gh-pages` branch (e.g. `git ls-remote --exit-code` with the same credentials)
# so a missing/expired GH token fails fast instead of after `helm package`.
# See .github/actions/npm-token-preflight for the pattern.
runs-on: ubuntu-latest
timeout-minutes: 15
env:
VERSION: ${{inputs.version}}
steps:
- name: Install Helm
# azure/setup-helm v4.3.1 pinned to commit SHA 1a275c3b69536ee54be43f2070a358922e12c8d4
# Source: https://github.com/Azure/setup-helm/releases/tag/v4.3.1
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4
with:
version: v3.19.4
- name: Validate version input
shell: bash
run: |
set -e
# Validate version input to prevent command injection
if ! echo "$VERSION" | grep -qE '^v?[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?(\+[A-Za-z0-9.-]+)?$'; then
echo "ERROR: Invalid version format: $VERSION"
echo "Expected SemVer format (e.g., v1.2.3, 1.2.3-alpha, 1.2.3+build)"
exit 1
fi
echo "Version validated: $VERSION"
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{inputs.version}}
fetch-tags: true
path: 'src'
fetch-depth: 0
- name: Checkout GH Pages branch
uses: actions/checkout@v6
with:
path: 'dest'
ref: 'gh-pages'
fetch-depth: 0
- name: Run lint on Helm chart
shell: bash
working-directory: src
run: |
set -e
cd deployment/helm_chart/opik
helm repo add mysql https://comet-ml.github.io/comet-mysql-helm/
helm repo add clickhouse-operator https://docs.altinity.com/clickhouse-operator
helm dependency build
helm lint --values values.yaml .
cd -
- name: Package helm chart
shell: bash
working-directory: src
run: |
set -e
cd deployment/helm_chart
helm package --version "$VERSION" --app-version "$VERSION" opik/ -u -d ../../../dest
cd -
echo "Copy updated README"
cp deployment/helm_chart/opik/README.md ../dest/.
- name: Restore file timestamps from git history
shell: bash
working-directory: dest
run: |
set -e
# Set file modification times based on git commit history
# This is important because helm repo index uses file mtime to set the 'created' field
# 1. Set timestamp for the newly packaged chart from source repo tag
NEW_CHART_FILE="opik-${VERSION}.tgz"
if [ -f "$NEW_CHART_FILE" ]; then
NEW_CHART_TIME=$(cd ../src && git log -1 --format="%cI" "$VERSION") || NEW_CHART_TIME=""
if [ -n "$NEW_CHART_TIME" ]; then
touch -d "$NEW_CHART_TIME" "$NEW_CHART_FILE"
echo "Set $NEW_CHART_FILE mtime to $NEW_CHART_TIME (from source tag)"
fi
fi
# 2. Restore timestamps for existing files from gh-pages git history
echo "Restoring file timestamps from git history..."
git ls-files -z | while read -r -d $'\0' file; do
if [ -f "$file" ]; then
TIME=$(git log -1 --format="%cI" -- "$file")
if [ -n "$TIME" ]; then
touch -d "$TIME" "$file"
echo "Set $file mtime to $TIME"
fi
fi
done
echo "File timestamps restored."
- name: Push New Files
shell: bash
working-directory: dest
env:
INDEX_FILE: index.yaml
run: |
set -e
# Run helm repo index which will overwrite created timestamps
helm repo index . --url https://raw.githubusercontent.com/comet-ml/opik/gh-pages/ --merge "${INDEX_FILE}"
# Fix timestamps again - helm repo index overwrites them based on file mtime
echo "=== Fixing timestamps based on file modification times ==="
CHARTS=$(yq eval '.entries[] | .[] | .urls[0]' "${INDEX_FILE}")
for CHART_URL in $CHARTS; do
FILENAME=$(basename "$CHART_URL")
if [ -f "$FILENAME" ]; then
MTIME=$(date -u -r "$FILENAME" +"%Y-%m-%dT%H:%M:%SZ")
echo "Setting $FILENAME timestamp to $MTIME"
yq eval "(.entries[][] | select(.urls[0] == \"$CHART_URL\") | .created) = \"$MTIME\"" -i --indent 2 "${INDEX_FILE}"
fi
done
git config user.name "Helm Updater"
git config user.email "actions@users.noreply.github.com"
git ls-files -o --exclude-standard -z | xargs -0 -r git add
git add "${INDEX_FILE}" README.md
if git diff --staged --quiet; then
echo "::warning title=helm publish::No changes to commit; chart ${VERSION} is already up to date in gh-pages."
else
git commit -m "Release Opik helm chart $VERSION"
git push
fi
- name: Summary
run: |
echo "Helm chart is published, version is : $VERSION" >> "$GITHUB_STEP_SUMMARY"
@@ -0,0 +1,116 @@
name: Python BE E2E Tests
run-name: "Python BE E2E Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
checks: write
pull-requests: write
on:
workflow_dispatch:
pull_request:
paths:
- 'apps/opik-python-backend/**'
- '.github/workflows/python_backend_e2e_tests.yml'
push:
branches:
- 'main'
paths:
- 'apps/opik-python-backend/**'
- '.github/workflows/python_backend_e2e_tests.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
OPIK_URL_OVERRIDE: http://localhost:8080
# The studio job processor streams subprocess logs to the stack's real Redis
# (password-protected, port-mapped to localhost by `opik.sh --port-mapping`).
# Not required for pass/fail (log flushing is best-effort), but without it the
# collector hits localhost:6379 unauthenticated and the log path goes dead.
REDIS_URL: redis://:opik@localhost:6379/0
# The Anthropic key is stored in the workspace by the tests (model-storage
# principle); the optimizer reaches the provider only through the backend
# gateway, never via this directly.
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# Keep the optimization short — we only need it to start, iterate, and finish.
OPTIMIZER_MAX_TRIALS: "1"
OPTSTUDIO_DATASET_SAMPLES: "4"
jobs:
run-python-be-e2e:
name: Python BE E2E
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Python 3.12
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Run Opik server
env:
OPIK_USAGE_REPORT_ENABLED: false
COMPOSE_BAKE: false
run: |
cd ${{ github.workspace }}
./opik.sh --backend --port-mapping --build
- name: Check Opik server availability
shell: bash
run: |
chmod +x ${{ github.workspace }}/tests_end_to_end/installer_utils/*.sh
cd ${{ github.workspace }}/deployment/docker-compose
${{ github.workspace }}/tests_end_to_end/installer_utils/check_docker_compose_pods.sh
${{ github.workspace }}/tests_end_to_end/installer_utils/check_backend.sh
- name: Install opik-python-backend
run: |
cd ${{ github.workspace }}/apps/opik-python-backend
pip install -r requirements.txt -r tests/test_requirements.txt
pip install .
- name: Run Python BE E2E tests
run: |
cd ${{ github.workspace }}/apps/opik-python-backend
# Retry transient LLM/network failures so a flaky provider call
# doesn't fail the run; the assertions themselves are deterministic.
# --log-cli-level streams logs live (incl. the subprocess error +
# traceback surfaced by process_optimizer_job) so failures are fully
# visible even though the optimization runs in a child process.
# --durations=0 logs how long each test took.
pytest tests/e2e -vv \
--reruns 2 --reruns-delay 5 \
--log-cli-level=INFO \
--durations=0 \
--junitxml=${{ github.workspace }}/python_be_e2e_results.xml
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action/linux@v2
if: always()
with:
action_fail: true
comment_mode: failures
check_name: Python BE E2E Tests Results
files: ${{ github.workspace }}/python_be_e2e_results.xml
- name: Keep BE logs on failure
if: failure()
run: |
cd ${{ github.workspace }}/deployment/docker-compose
docker compose logs backend > ${{ github.workspace }}/backend.log 2>&1 || true
docker compose logs python-backend > ${{ github.workspace }}/python-backend.log 2>&1 || true
- name: Upload BE logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: python-be-e2e-be-logs
path: |
${{ github.workspace }}/backend.log
${{ github.workspace }}/python-backend.log
if-no-files-found: ignore
@@ -0,0 +1,64 @@
name: "Python Backend Tests"
run-name: "Python Backend Tests on ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
checks: write
pull-requests: write
on:
pull_request:
paths:
- "apps/opik-python-backend/**"
- ".github/workflows/python_backend_tests.yml"
push:
branches:
- 'main'
paths:
- "apps/opik-python-backend/**"
- ".github/workflows/python_backend_tests.yml"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
run-python-backend-tests:
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: apps/opik-python-backend/
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt -r tests/test_requirements.txt
pip install .
- name: Run unit tests
env:
PYTHON_CODE_EXECUTOR_STRATEGY: process
PYTHON_CODE_EXECUTOR_EXEC_TIMEOUT_IN_SECS: 10
run: |
cd tests && pytest unit -v --durations=0 --junitxml=../test-results.xml
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action/linux@v2
if: always()
with:
action_fail: true
comment_mode: failures
check_name: Python Backend Tests Results
files: apps/opik-python-backend/test-results.xml
@@ -0,0 +1,159 @@
name: Python SDK Compatibility V1.x E2E Tests
run-name: "Python SDK Compatibility V1 E2E Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
checks: write
pull-requests: write
packages: write
on:
workflow_dispatch:
pull_request:
paths:
- 'sdks/python/**'
- 'apps/opik-backend/**'
- '.github/workflows/python_sdk_compatibility_v1_e2e_tests.yml'
push:
branches:
- 'main'
paths:
- 'sdks/python/**'
- 'apps/opik-backend/**'
- '.github/workflows/python_sdk_compatibility_v1_e2e_tests.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
OPIK_URL_OVERRIDE: http://localhost:8080
OPIK_CONSOLE_LOGGING_LEVEL: DEBUG
jobs:
select-matrix:
name: Select Python version matrix
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
python_versions: ${{ steps.pick.outputs.versions }}
steps:
- uses: actions/checkout@v6
with:
sparse-checkout: .github/actions/select-e2e-matrix
sparse-checkout-cone-mode: false
- uses: ./.github/actions/select-e2e-matrix
id: pick
with:
all-versions: ${{ vars.PYTHON_VERSIONS }}
match-regex: '^sdks/python/|^\.github/workflows/python_sdk_compatibility_v1_e2e_tests\.yml$'
github-token: ${{ secrets.GITHUB_TOKEN }}
build-opik:
uses: ./.github/workflows/build_e2e_docker.yaml
with:
include_guardrails: true
run-compatibility-v1-e2e:
needs: [select-matrix, build-opik]
name: Python SDK Compatibility V1.x E2E Tests ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
python_version: ${{ fromJSON(needs.select-matrix.outputs.python_versions) }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull Docker images from GHCR
env:
TAG: ${{ needs.build-opik.outputs.image_tag }}
run: |
docker pull "ghcr.io/comet-ml/opik/opik-backend:$TAG"
docker pull "ghcr.io/comet-ml/opik/opik-python-backend:$TAG"
docker pull "ghcr.io/comet-ml/opik/opik-guardrails-backend:$TAG"
docker tag "ghcr.io/comet-ml/opik/opik-backend:$TAG" ghcr.io/comet-ml/opik/opik-backend:latest
docker tag "ghcr.io/comet-ml/opik/opik-python-backend:$TAG" ghcr.io/comet-ml/opik/opik-python-backend:latest
docker tag "ghcr.io/comet-ml/opik/opik-guardrails-backend:$TAG" ghcr.io/comet-ml/opik/opik-guardrails-backend:latest
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Run latest Opik server
env:
OPIK_USAGE_REPORT_ENABLED: false
# Avoid Buildx Bake concurrent image export collisions in CI.
COMPOSE_BAKE: false
TOGGLE_RUNNERS_ENABLED: "true"
TOGGLE_AGENT_CONFIGURATION_ENABLED: "true"
OPIK_BACKEND_PULL_POLICY: never
PYTHON_BACKEND_PULL_POLICY: never
OPIK_GUARDRAILS_BACKEND_PULL_POLICY: never
run: |
cd ${{ github.workspace }}
./opik.sh --backend --port-mapping --guardrails
- name: Check Opik server availability
shell: bash
run: |
chmod +x ${{ github.workspace }}/tests_end_to_end/installer_utils/*.sh
cd ${{ github.workspace }}/deployment/docker-compose
echo "Check Docker pods are up"
${{ github.workspace }}/tests_end_to_end/installer_utils/check_docker_compose_pods.sh
echo "Check backend health"
${{ github.workspace }}/tests_end_to_end/installer_utils/check_backend.sh
- name: Install opik SDK
run: |
cd ${{ github.workspace }}/sdks/python
pip install .
- name: Install test requirements
run: |
cd ${{ github.workspace }}/sdks/python
pip install -r tests/test_requirements.txt
pip list
- name: Run compatibility v1 tests
run: |
cd ${{ github.workspace }}/sdks/python
pytest tests/e2e/compatibility_v1 -vv --durations=20 --junitxml=${{ github.workspace }}/test_results_compatibility_v1_${{matrix.python_version}}.xml
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action/linux@v2
if: always()
with:
action_fail: true
comment_mode: failures
check_name: Python SDK Compatibility V1 E2E Tests Results (Python ${{matrix.python_version}})
files: ${{ github.workspace }}/test_results_compatibility_v1_${{matrix.python_version}}.xml
- name: Keep BE log in case of failure
if: failure()
run: |
docker logs opik-backend-1 > ${{ github.workspace }}/opik-backend_compat_v1_p${{matrix.python_version}}.log
- name: Attach BE log
if: failure()
uses: actions/upload-artifact@v7
with:
name: opik-backend-log-compat-v1-p${{matrix.python_version}}
path: ${{ github.workspace }}/opik-backend_compat_v1_p${{matrix.python_version}}.log
- name: Stop opik server
if: always()
run: |
cd ${{ github.workspace }}
./opik.sh --stop --guardrails
+172
View File
@@ -0,0 +1,172 @@
name: Python SDK E2E Tests
run-name: "Python SDK E2E Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
checks: write
pull-requests: write
packages: write
on:
workflow_dispatch:
pull_request:
paths:
- 'sdks/python/**'
- 'apps/opik-backend/**'
- '.github/workflows/python_sdk_e2e_tests.yml'
push:
branches:
- 'main'
paths:
- 'sdks/python/**'
- 'apps/opik-backend/**'
- '.github/workflows/python_sdk_e2e_tests.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
OPIK_URL_OVERRIDE: http://localhost:8080
OPIK_CONSOLE_LOGGING_LEVEL: INFO
jobs:
select-matrix:
name: Select Python version matrix
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
python_versions: ${{ steps.pick.outputs.versions }}
steps:
- uses: actions/checkout@v6
with:
sparse-checkout: .github/actions/select-e2e-matrix
sparse-checkout-cone-mode: false
- uses: ./.github/actions/select-e2e-matrix
id: pick
with:
all-versions: ${{ vars.PYTHON_VERSIONS }}
match-regex: '^sdks/python/|^\.github/workflows/python_sdk_e2e_tests\.yml$'
github-token: ${{ secrets.GITHUB_TOKEN }}
build-opik:
uses: ./.github/workflows/build_e2e_docker.yaml
with:
include_guardrails: true
run-e2e:
needs: [select-matrix, build-opik]
name: Python SDK E2E Tests ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
python_version: ${{ fromJSON(needs.select-matrix.outputs.python_versions) }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull Docker images from GHCR
env:
TAG: ${{ needs.build-opik.outputs.image_tag }}
run: |
docker pull "ghcr.io/comet-ml/opik/opik-backend:$TAG"
docker pull "ghcr.io/comet-ml/opik/opik-python-backend:$TAG"
docker pull "ghcr.io/comet-ml/opik/opik-guardrails-backend:$TAG"
docker tag "ghcr.io/comet-ml/opik/opik-backend:$TAG" ghcr.io/comet-ml/opik/opik-backend:latest
docker tag "ghcr.io/comet-ml/opik/opik-python-backend:$TAG" ghcr.io/comet-ml/opik/opik-python-backend:latest
docker tag "ghcr.io/comet-ml/opik/opik-guardrails-backend:$TAG" ghcr.io/comet-ml/opik/opik-guardrails-backend:latest
- name: Setup Python ${{matrix.python_version}}
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python_version}}
- name: Run latest Opik server
env:
OPIK_USAGE_REPORT_ENABLED: false
# Avoid Buildx Bake concurrent image export collisions in CI.
COMPOSE_BAKE: false
TOGGLE_RUNNERS_ENABLED: "true"
OPIK_BACKEND_PULL_POLICY: never
PYTHON_BACKEND_PULL_POLICY: never
OPIK_GUARDRAILS_BACKEND_PULL_POLICY: never
run: |
cd ${{ github.workspace }}
./opik.sh --backend --port-mapping --guardrails
- name: Check Opik server availability
shell: bash
run: |
chmod +x ${{ github.workspace }}/tests_end_to_end/installer_utils/*.sh
cd ${{ github.workspace }}/deployment/docker-compose
echo "Check Docker pods are up"
${{ github.workspace }}/tests_end_to_end/installer_utils/check_docker_compose_pods.sh
echo "Check backend health"
${{ github.workspace }}/tests_end_to_end/installer_utils/check_backend.sh
- name: Install opik SDK
run: |
cd ${{ github.workspace }}/sdks/python
pip install .
- name: Run smoke tests
run: |
cd ${{ github.workspace }}/sdks/python/tests/e2e_smoke
./smoke_tests_runner.sh
- name: Install test requirements
run: |
cd ${{ github.workspace }}/sdks/python
pip install -r tests/test_requirements.txt
pip list
- name: Run tests
# -n 3 + --dist=loadfile distributes whole test files across three
# worker processes (one file per worker). Past ~3 the gains
# flatten — we become tail-bound on the slowest single file and
# the docker-compose backend on the same runner starts to thrash.
# E2E isolation hinges on --dist=loadfile: the per-module project
# name (generate_project_name("e2e", __name__)) is only collision-
# free if a single file is not split across workers.
# -p no:benchmark silences pytest-benchmark's auto-disable warning
# under xdist.
run: |
cd ${{ github.workspace }}/sdks/python
pytest tests/e2e --ignore=tests/e2e/test_guardrails.py --ignore=tests/e2e/compatibility_v1 -vv -n 3 --dist=loadfile -p no:benchmark --durations=20 --junitxml=${{ github.workspace }}/test_results_${{matrix.python_version}}.xml
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action/linux@v2
if: always()
with:
action_fail: true
comment_mode: failures
check_name: Python SDK E2E Tests Results (Python ${{matrix.python_version}})
files: ${{ github.workspace }}/test_results_${{matrix.python_version}}.xml
- name: Keep BE log in case of failure
if: failure()
run: |
docker logs opik-backend-1 > ${{ github.workspace }}/opik-backend_p${{matrix.python_version}}.log
- name: Attach BE log
if: failure()
uses: actions/upload-artifact@v7
with:
name: opik-backend-log-p${{matrix.python_version}}
path: ${{ github.workspace }}/opik-backend_p${{matrix.python_version}}.log
- name: Stop opik server
if: always()
run: |
cd ${{ github.workspace }}
./opik.sh --stop --guardrails
@@ -0,0 +1,81 @@
name: Python SDK Unit Tests
run-name: "Python SDK Unit Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
checks: write
pull-requests: write
on:
pull_request:
paths:
- 'sdks/python/**'
- '.github/workflows/python_sdk_unit_tests.yml'
push:
branches:
- 'main'
paths:
- 'sdks/python/**'
- '.github/workflows/python_sdk_unit_tests.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
jobs:
UnitTests:
name: Python SDK Unit Tests ${{matrix.python_version}}
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: sdks/python/
strategy:
fail-fast: false
matrix:
python_version: ${{ fromJSON(vars.PYTHON_VERSIONS || '["3.10","3.11","3.12","3.13","3.14"]') }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Print environment variables
run: env
- name: Print event object
run: cat "$GITHUB_EVENT_PATH"
- name: Print the PR title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "$PR_TITLE"
- name: Setup Python ${{ matrix.python_version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python_version }}
- name: Install opik
run: pip install .
- name: Install test requirements
run: |
cd ./tests
pip install --no-cache-dir --disable-pip-version-check -r test_requirements.txt -r ./unit/test_requirements.txt
pip list
- name: Running SDK Unit Tests
# Per-test timeout (thread method dumps all thread stacks via faulthandler)
# so a hung test fails fast with a traceback instead of a silent 20-min cancel.
run: python -m pytest -vv tests/unit/ --timeout=120 --timeout-method=thread --junitxml=${{ github.workspace }}/test_results.xml
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action/linux@v2
if: always()
with:
action_fail: true
comment_mode: failures
check_name: Python SDK Unit Tests Results (Python ${{matrix.python_version}})
files: ${{ github.workspace }}/test_results.xml
@@ -0,0 +1,77 @@
name: Quickstart Guide Snippets Tests
env:
OPIK_SENTRY_ENABLE: False
ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }}
ALLURE_JOB_RUN_ID: ${{ github.event.inputs.ALLURE_JOB_RUN_ID }}
ALLURE_ENDPOINT: https://comet.testops.cloud/
ALLURE_PROJECT_ID: 1
ALLURE_RESULTS: allure-results
on:
schedule:
- cron: '0 12 * * *' # runs every day at noon UTC
workflow_dispatch:
inputs:
ALLURE_JOB_RUN_ID:
type: string
required: false
description: Allure job run ID for grouping test reports
default: ''
run-name: Quickstart Guide Snippets Tests
jobs:
run_tests:
name: "Quickstart Guide Snippets Tests"
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repo
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install Opik
run: pip install opik
- name: Install Test Dependencies
run: |
pip install -r ${{ github.workspace }}/tests_end_to_end/test_requirements.txt
pip install -r ${{ github.workspace }}/tests_end_to_end/integrations_requirements.txt
pip install allure-pytest
playwright install
- name: Install allurectl
uses: allure-framework/setup-allurectl@v1
- name: Install Opik (Local)
env:
OPIK_USAGE_REPORT_ENABLED: false
run: |
cd ${{ github.workspace }}
./opik.sh
- name: Run suite
env:
OPIK_BASE_URL: http://localhost:5173
OPIK_TEST_WORKSPACE: default
OPIK_TEST_PROJECT_NAME: Quickstart
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.BEDROCK_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.BEDROCK_AWS_SECRET_ACCESS_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
cd ${{ github.workspace }}/tests_end_to_end
export PYTHONPATH='.'
allurectl watch -- pytest -s tests/QuickstartGuide --alluredir="${ALLURE_RESULTS}" --browser chromium
- name: Stop Opik server (Local)
if: always()
run: |
cd ${{ github.workspace }}
./opik.sh --stop
+21
View File
@@ -0,0 +1,21 @@
name: Release Drafter
on:
push:
# branches to consider in the event; optional, defaults to all
branches:
- main
jobs:
update_release_draft:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
# Drafts your next Release notes as Pull Requests are merged into "master"
- uses: release-drafter/release-drafter@v7
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
# with:
# config-name: my-config.yml
# disable-autolabeler: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,82 @@
name: "Release with Release-n-Deploy-Agent"
run-name: "Release from ${{ github.ref_name }}"
on:
workflow_dispatch:
inputs:
resumeUrl:
description: 'URL to call to resume release n deploy'
required: true
trackingId:
description: 'UUID for tracking this release step'
required: true
type: string
reuse_existing_tag:
description: 'Reuse an existing git tag for this version instead of creating + pushing a new one. Use to replay a release whose tag was already created (e.g. by a prior failed run).'
required: false
type: boolean
default: false
jobs:
trigger-release:
uses: ./.github/workflows/release.yaml # Call the actual release workflow
secrets: inherit
with:
reuse_existing_tag: ${{ inputs.reuse_existing_tag }}
notify-release-n-deploy-agent:
runs-on: ubuntu-latest
timeout-minutes: 90
needs: trigger-release
if: ${{ always() }}
steps:
- name: Determine release status
id: status
run: |
if [ "${{ needs.trigger-release.result }}" == "success" ]; then
echo "status=completed" >> "$GITHUB_OUTPUT"
echo "conclusion=success" >> "$GITHUB_OUTPUT"
else
echo "status=completed" >> "$GITHUB_OUTPUT"
echo "conclusion=failure" >> "$GITHUB_OUTPUT"
fi
- name: Notify Release-n-Deploy-Agent
if: ${{ inputs.resumeUrl != '' }}
run: |
echo "All inputs: ${{ toJSON(inputs) }}"
echo "resumeUrl input: '${{ inputs.resumeUrl }}'"
# Get actual release outputs
VERSION="${{ needs.trigger-release.outputs.version }}"
STATUS="${{ steps.status.outputs.status }}"
CONCLUSION="${{ steps.status.outputs.conclusion }}"
echo "📦 Release completed with version: $VERSION"
echo "Status: $STATUS, Conclusion: $CONCLUSION"
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"release": {
"id": "${{ github.run_id }}",
"releaseTag": "'$VERSION'"
},
"trackingId": "${{ inputs.trackingId }}",
"workflow_run": {
"id": "${{ github.event.workflow_run.id }}",
"name": "${{ github.event.workflow_run.name }}",
"status": "'$STATUS'",
"conclusion": "'$CONCLUSION'",
"run_started_at": "${{ github.event.workflow_run.run_started_at }}",
"updated_at": "${{ github.event.workflow_run.updated_at }}",
"html_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
},
"repository": {
"full_name": "${{ github.repository }}"
},
"sender": {
"login": "${{ github.actor }}"
}
}' \
${{ inputs.resumeUrl }}
+259
View File
@@ -0,0 +1,259 @@
# Internal release
name: "Release"
run-name: "Release from ${{github.ref_name}} by @${{ github.actor }}"
on:
workflow_dispatch:
inputs:
reuse_existing_tag:
type: boolean
required: false
default: false
description: "Reuse an existing git tag for this version instead of creating + pushing a new one. Use to replay a release whose tag was already created (e.g. by a prior failed run)."
workflow_call: # in order to make this action been called from outside
inputs:
reuse_existing_tag:
type: boolean
required: false
default: false
description: "Reuse an existing git tag for this version instead of creating + pushing a new one."
outputs:
version:
description: 'The release version'
value: ${{ jobs.set-version.outputs.version }}
jobs:
set-version:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set version
id: version
run: |
echo "version=$(cat version.txt)" | tee -a "$GITHUB_OUTPUT"
create-git-tag:
needs:
- set-version
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Create git tag
env:
RELEASE_VERSION: ${{ needs.set-version.outputs.version }}
REUSE_EXISTING_TAG: ${{ inputs.reuse_existing_tag }}
run: |
set -x
git config --local user.email "github-actions@comet.com"
git config --local user.name "github-actions"
if [ "${REUSE_EXISTING_TAG}" = "true" ]; then
# Resolve the SHA the remote tag points to and surface it in the
# warning. `reuse_existing_tag=true` is an operator trust statement
# ("I trust this tag"). Printing the SHA lets the operator sanity-
# check from the Actions log that the tag points to the commit they
# expect — and cancel the run if it doesn't — without us having to
# take a stance on what 'correct' means here (e.g., an ancestor
# check would falsely reject legitimate replays after a main
# rebase). See PR #7124 review thread for the threat-model
# discussion.
TAG_SHA=$(git ls-remote --tags origin "${RELEASE_VERSION}" | awk '{print $1}')
if [ -n "${TAG_SHA}" ]; then
echo "::warning title=create-git-tag::reuse_existing_tag=true and origin already has tag ${RELEASE_VERSION} (commit ${TAG_SHA}); skipping create+push. Build will pin to this commit — verify it's the source you intend to release."
exit 0
fi
echo "::error title=create-git-tag::reuse_existing_tag=true but origin does not have tag ${RELEASE_VERSION}. Refusing to silently create a new one."
exit 1
fi
git tag "${RELEASE_VERSION}"
git push --no-verify origin "${RELEASE_VERSION}"
release-apps:
needs:
- set-version
- create-git-tag
uses: ./.github/workflows/build_apps.yml
secrets: inherit
with:
version: ${{needs.set-version.outputs.version}}
is_release: true
# Caller-passes ref: build_apps.yml already exposes a ref input (defaults to inputs.ref || github.ref).
# TS SDK workflows in OPIK-6627 used callee-derives because they lacked one. See OPIK-6633.
ref: refs/tags/${{needs.set-version.outputs.version}}
release-sdk:
needs:
- set-version
- create-git-tag
uses: ./.github/workflows/build_and_publish_sdk.yaml
secrets: inherit
with:
version: ${{needs.set-version.outputs.version}}
is_release: true
release-typescript-sdk:
needs:
- set-version
- create-git-tag
uses: ./.github/workflows/typescript_sdk_publish.yml
secrets: inherit
with:
version: ${{needs.set-version.outputs.version}}
is_release: true
skip_commit_push: true
release-typescript-sdk-integrations:
needs:
- set-version
- create-git-tag
uses: ./.github/workflows/typescript_sdk_integration_publish.yml
secrets: inherit
with:
version: ${{needs.set-version.outputs.version}}
is_release: true
skip_commit_push: true
publish-helm-chart:
needs:
- set-version
- create-git-tag
uses: ./.github/workflows/publish_helm_chart.yaml
secrets: inherit
with:
version: ${{needs.set-version.outputs.version}}
create-github-release:
needs:
- set-version
- create-git-tag
- release-apps
- release-sdk
- release-typescript-sdk
- release-typescript-sdk-integrations
- publish-helm-chart
runs-on: ubuntu-latest
steps:
- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{needs.set-version.outputs.version}}
generate_release_notes: true
- name: Delete Release Draft
uses: hugo19941994/delete-draft-releases@v1.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
commit-all-version-changes:
needs:
- set-version
- release-typescript-sdk
- release-typescript-sdk-integrations
- publish-helm-chart
- create-github-release
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
token: ${{ secrets.GH_PAT_TO_ACCESS_GITHUB_API }}
- name: Setup Node.js for TypeScript version update
uses: actions/setup-node@v4
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"
- name: Update TypeScript SDK version
shell: bash
working-directory: sdks/typescript
run: |
npm version ${{ needs.set-version.outputs.version }} --no-git-tag-version
- name: Update TypeScript SDK Integrations versions
shell: bash
run: |
for integration in opik-openai opik-gemini opik-langchain opik-vercel opik-otel; do
if [ -d "sdks/typescript/src/opik/integrations/$integration" ]; then
echo "Updating version for $integration"
cd "sdks/typescript/src/opik/integrations/$integration"
npm version ${{ needs.set-version.outputs.version }} --no-git-tag-version
cd - > /dev/null
fi
done
- name: Update Chart.yaml with release version
shell: bash
run: |
set -x
cd deployment/helm_chart/opik
sed -i "s/^version:.*/version: ${{ needs.set-version.outputs.version }}/" Chart.yaml
sed -i "s/^appVersion:.*/appVersion: ${{ needs.set-version.outputs.version }}/" Chart.yaml
echo "Updated Chart.yaml version and appVersion to: ${{ needs.set-version.outputs.version }}"
grep -E "^version:|^appVersion:" Chart.yaml
cd -
# The chart README badges are derived from Chart.yaml by helm-docs, so
# regenerate here (git-push: false) to fold the update into the version
# commit below. Without this the README lags one release and the next
# chart-touching PR trips the helm-docs / update_helm_readme.yaml gate.
# Same action + inputs as update_helm_readme.yaml, keeping CI and the
# local pre-commit hook in agreement.
- name: Regenerate Helm chart README
uses: losisin/helm-docs-github-action@v1
with:
chart-search-root: deployment/helm_chart/opik
git-push: false
- name: Bump version.txt for next release
id: update_version
shell: bash
run: |
set -x
BASE_VERSION=$(tr -d '\r' < version.txt | xargs)
IFS='.' read -r -a version_parts <<< "$BASE_VERSION"
# Ensure all version parts are set
for i in {0..2}; do
version_parts[i]=${version_parts[i]:-0}
done
# Convert to decimal before incrementing to avoid octal interpretation issues
version_parts[2]=$((10#${version_parts[2]} + 1))
new_version="${version_parts[0]}.${version_parts[1]}.${version_parts[2]}"
# Update version file with the new version
echo "$new_version" > version.txt
echo "new_version=${new_version}" >> "$GITHUB_OUTPUT"
- name: Commit all version changes
run: |
git config --local user.email "github-actions@comet.com"
git config --local user.name "github-actions"
# Add all modified version files
git add sdks/typescript/package.json sdks/typescript/package-lock.json
git add sdks/typescript/src/opik/integrations/*/package.json sdks/typescript/src/opik/integrations/*/package-lock.json
git add deployment/helm_chart/opik/Chart.yaml
git add deployment/helm_chart/opik/README.md
git add version.txt
# Check if there are any changes to commit
if git diff --staged --quiet; then
echo "No changes to commit"
else
git commit -m "Update versions to ${{ needs.set-version.outputs.version }} and bump base version to ${{ steps.update_version.outputs.new_version }}"
git push origin main
fi
echo "Version updated to ${{ steps.update_version.outputs.new_version }} on main" >> "$GITHUB_STEP_SUMMARY"
+37
View File
@@ -0,0 +1,37 @@
name: Scout Feedback Sync
on:
# GitHub emits no event for comment reactions, so poll on a schedule.
schedule:
- cron: '0 * * * *' # hourly
# Allow manual runs (and an optional wider scan window for backfills).
workflow_dispatch:
inputs:
since_days:
description: How many days back to scan issues for reactions
required: false
default: '7'
type: string
# Don't let overlapping syncs double up.
concurrency:
group: scout-feedback-sync
cancel-in-progress: false
jobs:
sync-feedback:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
issues: read
contents: read
steps:
- name: Sync reactions to Opik
uses: comet-ml/scout-repo-agent/actions/feedback@main
with:
github_token: ${{ github.token }}
since_days: ${{ github.event.inputs.since_days || '7' }}
env:
OPIK_API_KEY: ${{ secrets.SCOUT_OPIK_API_KEY }}
OPIK_WORKSPACE: ${{ vars.SCOUT_OPIK_WORKSPACE }}
+38
View File
@@ -0,0 +1,38 @@
name: Scout Issue Triage
on:
issues:
types: [opened]
workflow_dispatch:
inputs:
issue_number:
description: Issue number to triage
required: true
type: number
# One Scout run per issue at a time
concurrency:
group: scout-issue-${{ github.event.issue.number || github.event.inputs.issue_number }}
cancel-in-progress: false
jobs:
triage:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
issues: write
contents: read
steps:
- name: Run Scout
uses: comet-ml/scout-repo-agent@main
with:
anthropic_api_key: ${{ secrets.SCOUT_ANTHROPIC_API_KEY }}
github_token: ${{ github.token }}
env:
SCOUT_ESCALATION_TAG: ${{ vars.SCOUT_ESCALATION_TAG }}
SCOUT_GITHUB_REPO_OWNER: ${{ vars.SCOUT_GITHUB_REPO_OWNER }}
SCOUT_GITHUB_REPO_NAME: ${{ vars.SCOUT_GITHUB_REPO_NAME }}
OPIK_API_KEY: ${{ secrets.SCOUT_OPIK_API_KEY }}
OPIK_WORKSPACE: ${{ vars.SCOUT_OPIK_WORKSPACE }}
ISSUE_NUMBER: ${{ github.event.issue.number || github.event.inputs.issue_number }}
@@ -0,0 +1,183 @@
# The workflow to run tests for third-party libraries integrations that require OPIK server running
#
name: SDK E2E Libraries Integration Tests
permissions:
contents: read
packages: write
run-name: "SDK E2E Libraries Integration Tests ${{ github.ref_name }} by @${{ github.actor }}"
on:
workflow_dispatch:
inputs:
libs:
description: "Choose specific library to test against or all"
required: true
type: choice
options:
- all
- adk
- litellm
- opentelemetry
- pydantic_ai
schedule:
- cron: "0 0 */2 * *"
pull_request:
paths:
- 'sdks/python/**'
- '.github/workflows/sdk-e2e-library-tests.yaml'
push:
branches:
- 'main'
paths:
- 'sdks/python/**'
- '.github/workflows/sdk-e2e-library-tests.yaml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
OPIK_ENABLE_LITELLM_MODELS_MONITORING: True # unlike other workflows, here we need it for tests
OPIK_SENTRY_ENABLE: False
SLACK_WEBHOOK_URL: ${{ secrets.ACTION_MONITORING_SLACK }}
LIBS: ${{ github.event.inputs.libs != '' && github.event.inputs.libs || 'all' }}
OPIK_URL_OVERRIDE: http://localhost:8080
jobs:
has_needed_secrets:
name: Check Secrets
runs-on: ubuntu-latest
outputs:
has_secrets: ${{ steps.init.outputs.has_secrets }}
steps:
- name: Print has secrets into output
id: init
run: |
echo "has_secrets=${{ secrets.OPENAI_API_KEY != '' }}" >> "$GITHUB_OUTPUT"
missed_api_key_warning:
name: Missed OpenAI API Key Warning
needs: [has_needed_secrets]
runs-on: ubuntu-latest
if: ${{ needs.has_needed_secrets.outputs.has_secrets == 'false' }}
steps:
- name: Print disabled message
run: |
echo "::warning::SDK E2E Libraries Integration Tests workflow is disabled because OPENAI_API_KEY is not set"
build-opik:
needs: [has_needed_secrets]
if: ${{ needs.has_needed_secrets.outputs.has_secrets == 'true' }}
uses: ./.github/workflows/build_e2e_docker.yaml
run_e2e_lib_integration:
name: E2E Lib Integration Python ${{matrix.python_version}}
needs: [has_needed_secrets, build-opik]
runs-on: ubuntu-latest
timeout-minutes: 30
if: ${{ needs.has_needed_secrets.outputs.has_secrets == 'true' }}
strategy:
fail-fast: false
matrix:
python_version: ${{ fromJSON(vars.PYTHON_VERSIONS) }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull Docker images from GHCR
env:
TAG: ${{ needs.build-opik.outputs.image_tag }}
run: |
docker pull "ghcr.io/comet-ml/opik/opik-backend:$TAG"
docker pull "ghcr.io/comet-ml/opik/opik-python-backend:$TAG"
docker tag "ghcr.io/comet-ml/opik/opik-backend:$TAG" ghcr.io/comet-ml/opik/opik-backend:latest
docker tag "ghcr.io/comet-ml/opik/opik-python-backend:$TAG" ghcr.io/comet-ml/opik/opik-python-backend:latest
- name: Install uv and set the Python version ${{ matrix.python_version }}
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python_version }}
- name: Make LIBS variable global (workaround for cron)
id: init
run: |
echo "LIBS=${{ env.LIBS }}" >> "$GITHUB_OUTPUT"
- name: Run latest Opik server
env:
OPIK_USAGE_REPORT_ENABLED: false
# Avoid Buildx Bake concurrent image export collisions in CI.
COMPOSE_BAKE: false
OPIK_BACKEND_PULL_POLICY: never
PYTHON_BACKEND_PULL_POLICY: never
shell: bash
run: |
cd ${{ github.workspace }}
./opik.sh --backend --port-mapping
- name: Check Opik server availability
shell: bash
run: |
chmod +x ${{ github.workspace }}/tests_end_to_end/installer_utils/*.sh
cd ${{ github.workspace }}/deployment/docker-compose
echo "Check Docker pods are up"
${{ github.workspace }}/tests_end_to_end/installer_utils/check_docker_compose_pods.sh
echo "Check backend health"
${{ github.workspace }}/tests_end_to_end/installer_utils/check_backend.sh
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: '${{ secrets.GCP_CREDENTIALS_JSON }}'
- name: ADK Tests Python ${{ matrix.python_version }}
if: contains(fromJSON('["adk", "all"]'), env.LIBS)
env:
GOOGLE_CLOUD_LOCATION: global
GOOGLE_CLOUD_PROJECT: opik-sdk-tests
GOOGLE_GENAI_USE_VERTEXAI: TRUE
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
uses: ./.github/actions/install_opik_and_run_e2e_lib_integration_tests
with:
python_version: ${{ matrix.python_version }}
library_name: adk
# This workflow is used to test the external LiteLLM integration that lives in litellm repository.
# However, there is another, internal, integration with LiteLLM that lives in opik.integrations.litellm, it's being tested
# in a separate workflow: .github/workflows/lib-litellm-tests.yml
- name: LiteLLM Tests Python ${{ matrix.python_version }}
if: contains(fromJSON('["litellm", "all"]'), env.LIBS)
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
uses: ./.github/actions/install_opik_and_run_e2e_lib_integration_tests
with:
python_version: ${{ matrix.python_version }}
library_name: litellm
- name: OpenTelemetry Tests Python ${{ matrix.python_version }}
if: contains(fromJSON('["opentelemetry", "all"]'), env.LIBS)
uses: ./.github/actions/install_opik_and_run_e2e_lib_integration_tests
with:
python_version: ${{ matrix.python_version }}
library_name: opentelemetry
- name: PydanticAI Tests Python ${{ matrix.python_version }}
if: contains(fromJSON('["pydantic_ai", "all"]'), env.LIBS)
uses: ./.github/actions/install_opik_and_run_e2e_lib_integration_tests
with:
python_version: ${{ matrix.python_version }}
library_name: pydantic_ai
- name: Stop opik server
run: |
cd ${{ github.workspace }}
./opik.sh --stop
@@ -0,0 +1,109 @@
name: SDKs Generate OpenAPI spec and Fern code
on:
schedule:
- cron: '0 0 * * 1-5' # Runs at midnight UTC, Monday to Friday
workflow_dispatch: # Allows manual trigger
concurrency:
group: fern-auto-update
cancel-in-progress: true
jobs:
update-open-api-spec-and-fern-code:
runs-on: ubuntu-latest
permissions:
contents: write
timeout-minutes: 15
steps:
- name: Checkout Repository
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'corretto'
cache: maven
- name: Install Fern
run: npm install -g fern-api@4.71.5
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install Opik Optimizer SDK (for Fern docs)
run: |
python -m pip install --upgrade pip
pip install -e sdks/opik_optimizer
- name: Set branch name
run: |
echo "BRANCH_NAME=github-actions/NA-automatically-update-openapi-spec-and-fern-code" >> "$GITHUB_ENV"
- name: Run Generate OpenAPI spec and Fern code
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
run: |
set -e
cd apps/opik-backend
mvn clean
cd -
./scripts/generate_openapi.sh
- name: Generate Opik Optimizer Fern docs
run: |
python sdks/opik_optimizer/scripts/generate_fern_docs.py --write
- name: Check for changes
id: check_changes
run: |
if git diff --quiet; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Commit files
if: steps.check_changes.outputs.has_changes == 'true'
run: |
set -ex
git config --local user.email "github-actions@comet.com"
git config --local user.name "Github Actions (${{ github.actor }})"
git add sdks/python/src/opik/rest_api/
git add sdks/typescript/src/opik/rest_api/
git add sdks/code_generation/fern/openapi/openapi.yaml
git add apps/opik-documentation/documentation/fern/openapi/opik.yaml
git add apps/opik-documentation/documentation/fern/docs/agent_optimization/opik_optimizer/reference.mdx
git commit -m "[NA] [SDK] [DOCS] Update automatically OpenAPI spec and Fern code"
- name: Create Pull Request
if: steps.check_changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.COMET_ACTION_CREATE_PR }}
branch: ${{ env.BRANCH_NAME }}
title: "[NA] [SDK] [DOCS] Update automatically OpenAPI spec and Fern code"
body: |
## Details
Automated daily update of OpenAPI spec and Fern code after running `./scripts/generate_openapi.sh` and `./sdks/opik_optimizer/scripts/generate_fern_docs.py`.
## Change checklist
- [ ] User facing changes
- [X] Documentation updated
## Issues
N/A
## Testing
- Passed CI
## Documentation
- https://buildwithfern.com/learn/cli-api-reference/cli-reference/commands#fern-generate
base: main
@@ -0,0 +1,71 @@
name: Update Span Cost Daily
on:
schedule:
- cron: '0 0 * * 1-5' # Runs at midnight UTC, Monday to Friday
workflow_dispatch: # Allows manual trigger
jobs:
update-span-cost:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout Repository
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set branch name and update the file
run: |
echo "BRANCH_NAME=github-actions/NA-automatically-update-model-prices-file-$(date +%Y-%m-%d-%H-%M-%S)" >> "$GITHUB_ENV"
curl -o apps/opik-backend/src/main/resources/model_prices_and_context_window.json https://raw.githubusercontent.com/BerriAI/litellm/refs/heads/main/model_prices_and_context_window.json
cp apps/opik-backend/src/main/resources/model_prices_and_context_window.json apps/opik-frontend/src/data/model_prices_and_context_window.json
- name: Check for changes
id: check_changes
run: |
if git diff --quiet; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Commit files
if: steps.check_changes.outputs.has_changes == 'true'
run: |
set -ex
git config --local user.email "github-actions@comet.com"
git config --local user.name "Github Actions (${{ github.actor }})"
git add apps/opik-backend/src/main/resources/model_prices_and_context_window.json
git add apps/opik-frontend/src/data/model_prices_and_context_window.json
git commit -m "[NA] [BE] Update model prices file"
- name: Create Pull Request
if: steps.check_changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.COMET_ACTION_CREATE_PR }}
branch: ${{ env.BRANCH_NAME }}
title: "[NA] [BE] Update model prices file"
body: |
## Details
Automated update of `model_prices_and_context_window.json` file and regeneration of supported models documentation.
**Files updated:**
- `apps/opik-backend/src/main/resources/model_prices_and_context_window.json` - Latest model pricing data from LiteLLM
- `apps/opik-frontend/src/data/model_prices_and_context_window.json` - Frontend copy kept in sync with backend
## Change checklist
- [X] User facing change
- [X] Documentation update
## Issues
N/A
## Testing
- Passed CI
## Documentation
- https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json
base: main
+174
View File
@@ -0,0 +1,174 @@
name: Sync Provider Models Daily
on:
schedule:
- cron: '30 0 * * 1-5' # 30 min after model prices sync, Monday to Friday
workflow_dispatch:
inputs:
force_regen:
description: 'Regenerate and re-upload YAML even if no new models were found'
type: boolean
required: false
default: false
permissions:
contents: write
pull-requests: write
jobs:
sync-models:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout Repository
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: pip install requests
- name: Set branch name
run: |
echo "BRANCH_NAME=github-actions/NA-sync-provider-models-$(date +%Y-%m-%d-%H-%M-%S)" >> "$GITHUB_ENV"
- name: Run sync script
id: sync
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
FORCE_REGEN_FLAG: ${{ inputs.force_regen == true && '--force-regen' || '' }}
run: |
set +e -o pipefail
# $FORCE_REGEN_FLAG is intentionally unquoted: it holds either
# `--force-regen` (a single flag) or the empty string. Quoting it
# would pass a literal empty argument to python, which is harmless
# here but conceptually wrong — the variable is acting as an
# optional argv slot, not a single value.
# shellcheck disable=SC2086
python scripts/sync_provider_models.py $FORCE_REGEN_FLAG 2>sync_stderr.txt | tee sync_summary.txt
EXIT_CODE=${PIPESTATUS[0]}
set -e
echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT"
# Capture summary for PR body (multiline output)
{
echo "summary<<EOF"
cat sync_summary.txt
echo "EOF"
} >> "$GITHUB_OUTPUT"
if [ "$EXIT_CODE" -eq 1 ]; then
echo "No new models found, skipping PR creation."
fi
rm -f sync_summary.txt sync_stderr.txt
- name: Print summary
if: always() && steps.sync.outputs.summary != ''
run: |
{
echo "## Sync Summary"
echo '```'
echo "${{ steps.sync.outputs.summary }}"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Check for changes
if: steps.sync.outputs.exit_code == '0'
id: check_changes
run: |
if git diff --quiet; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Commit files
if: steps.check_changes.outputs.has_changes == 'true'
run: |
set -ex
git config --local user.email "github-actions@comet.com"
git config --local user.name "Github Actions (${{ github.actor }})"
git add apps/opik-backend/src/main/java/com/comet/opik/infrastructure/llm/
git add apps/opik-frontend/src/types/providers.ts
git add apps/opik-frontend/src/constants/providerModels.ts
git add apps/opik-backend/src/main/resources/llm-models-default.yaml
git commit -m "[NA] [BE][FE] chore: sync provider model definitions"
- name: Configure AWS credentials
if: steps.sync.outputs.exit_code == '0'
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_OPIK_CDN_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_OPIK_CDN_SECRET }}
aws-region: us-east-1
role-to-assume: ${{ vars.AWS_OPIK_CDN_ROLE }}
role-session-name: sync-provider-models
- name: Upload YAML to S3
if: steps.sync.outputs.exit_code == '0'
run: |
aws s3 cp \
apps/opik-backend/src/main/resources/llm-models-default.yaml \
${{ vars.AWS_OPIK_CDN_BUCKET }}/llm-models-default.yaml \
--cache-control "max-age=300"
- name: Invalidate CloudFront distribution
if: steps.sync.outputs.exit_code == '0'
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.AWS_OPIK_CDN_DISTRIBUTION }} \
--paths "/opik/llm-models-default.yaml"
- name: Create Pull Request
if: steps.check_changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.COMET_ACTION_CREATE_PR }}
branch: ${{ env.BRANCH_NAME }}
title: "[NA] [BE][FE] chore: sync provider model definitions"
body: |
## Details
Automated sync of LLM provider model definitions from source APIs and prices JSON.
**Sync summary:**
```
${{ steps.sync.outputs.summary }}
```
## Change checklist
- [x] User facing
- [ ] Documentation update
## Issues
- NA
## AI-WATERMARK
AI-WATERMARK: yes
- If yes:
- Tools: GitHub Actions (`sync_provider_models.yml`)
- Model(s): N/A (scripted automation)
- Scope: Automated model list sync
- Human verification: Requires manual review before merge
## Testing
- Script dry-run passed in CI
- Changes are add-only; stale/deprecated models flagged for manual review
## Documentation
N/A
base: main
+62
View File
@@ -0,0 +1,62 @@
name: Check Documentation Links
env:
ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }}
ALLURE_JOB_RUN_ID: ${{ github.event.inputs.ALLURE_JOB_RUN_ID }}
ALLURE_ENDPOINT: https://comet.testops.cloud/
ALLURE_PROJECT_ID: 1
ALLURE_RESULTS: allure-results
on:
schedule:
- cron: '0 12 * * *' # Run daily at noon UTC
workflow_dispatch:
inputs:
ALLURE_JOB_RUN_ID:
description: Allure run ID to attach test results to
required: false
type: string
jobs:
check-links:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install pytest and allure
run: |
pip install pytest allure-pytest
- name: Install allurectl
uses: allure-framework/setup-allurectl@v1
- name: Install Test Dependencies
run: |
pip install -r ${{ github.workspace }}/tests_end_to_end/test_requirements.txt
pip install opik
playwright install chromium
- name: Install linkinator
run: |
npm install linkinator
- name: Run linkinator
run: |
npx linkinator https://www.comet.com/docs/opik/ --recurse --format csv > ${{ github.workspace }}/tests_end_to_end/tests/Documentation/output.csv
- name: Check for broken Comet links
run: |
cd ${{ github.workspace }}/tests_end_to_end
export PYTHONPATH='.'
allurectl watch -- pytest -s tests/Documentation/test_links.py --browser chromium --alluredir="${ALLURE_RESULTS}"
@@ -0,0 +1,188 @@
name: Trigger Test Environment Deployment
run-name: ${{ github.event.label.name == 'test-environment' && format('Trigger Test Environment Deployment for PR {0} by {1}', github.event.number, github.actor) || 'Test Environment Not Triggered' }}
on:
pull_request:
types: [labeled]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
packages: write
jobs:
other-labels:
if: github.event_name == 'pull_request' && github.event.label.name != 'test-environment'
runs-on: ubuntu-latest
steps:
- name: Skip
run: |
echo "Skipping deployment of test environment for this PR"
start:
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'test-environment'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-tag.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.ref }}
- name: Get latest tag from main at merge-base
id: get-tag
run: |
# Fetch main branch
git fetch origin main
# Find the merge-base (where this branch diverged from main)
MERGE_BASE=$(git merge-base HEAD origin/main)
echo "Merge base: $MERGE_BASE"
# Get the tag at or before the merge-base
BASE_TAG=$(git describe --tags --abbrev=0 "$MERGE_BASE" 2>/dev/null || echo "")
if [ -z "$BASE_TAG" ]; then
echo "❌ No tag found at merge-base"
exit 1
fi
echo "The tag at merge-base: $BASE_TAG"
echo "version=${BASE_TAG}" | tee -a "$GITHUB_OUTPUT"
echo "**Base Version**: \`${BASE_TAG}\`" >> "$GITHUB_STEP_SUMMARY"
- name: Comment on PR - Deployment Started
uses: actions/github-script@v7
# HEAD_REF must be passed via env: — interpolating `${{ github.event.pull_request.head.ref }}`
# directly into the script body would let a crafted branch name break out of the string
# context. See: https://docs.github.com/en/actions/reference/security/secure-use#good-practices-for-mitigating-script-injection-attacks
env:
HEAD_REF: ${{ github.event.pull_request.head.ref }}
BASE_VERSION: ${{ steps.get-tag.outputs.version }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.issues.createComment({
issue_number: Number('${{ github.event.number }}'),
owner: context.repo.owner,
repo: context.repo.repo,
body: `🔄 **Test environment deployment process has started**
**Phase 1**: Deploying base version \`${process.env.BASE_VERSION}\` (from main branch) if environment doesn't exist
**Phase 2**: Building new images from PR branch \`${process.env.HEAD_REF}\`
**Phase 3**: Will deploy newly built version after build completes
You can monitor the progress [here](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}).`
});
build-apps:
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'test-environment'
needs:
- start
uses: ./.github/workflows/build_apps.yml
secrets: inherit
with:
version: ""
is_release: false
is_adhoc: true
build_guardrails: false
ref: ${{ github.event.pull_request.head.ref }}
trigger-deploy-base-version:
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'test-environment'
needs:
- start
runs-on: ubuntu-latest
steps:
- name: Trigger deployment with base version
uses: actions/github-script@v7
env:
VERSION_NAME: ${{ needs.start.outputs.version }}
with:
github-token: ${{ secrets.DEPLOYMENT_TRIGGER_TOKEN }}
script: |
const response = await github.rest.repos.createDispatchEvent({
owner: 'comet-ml',
repo: 'comet-deployment',
event_type: 'deploy-opik-test-env',
client_payload: {
pr_number: '${{ github.event.number }}',
version_name: process.env.VERSION_NAME,
is_base_version: true
}
});
console.log('✅ Repository dispatch sent successfully:', response.status);
console.log('📦 Deploying base version:', process.env.VERSION_NAME);
console.log('⚠️ Will skip if namespace already exists');
console.log('📊 Monitor deployment progress: https://github.com/comet-ml/comet-deployment/actions/workflows/deploy_opik_adhoc_env.yaml');
console.log('🔗 Deployment will verify the version');
- name: Handle trigger failure
if: failure()
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.issues.createComment({
issue_number: Number('${{ github.event.number }}'),
owner: context.repo.owner,
repo: context.repo.repo,
body: `❌ **Failed to trigger base version deployment**
Please check the [workflow logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for more details.`
});
trigger-deploy-new-version:
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'test-environment'
needs:
- build-apps
- trigger-deploy-base-version
runs-on: ubuntu-latest
steps:
- name: Trigger deployment with new version
uses: actions/github-script@v7
env:
VERSION_NAME: ${{ needs.build-apps.outputs.version }}
with:
github-token: ${{ secrets.DEPLOYMENT_TRIGGER_TOKEN }}
script: |
const response = await github.rest.repos.createDispatchEvent({
owner: 'comet-ml',
repo: 'comet-deployment',
event_type: 'deploy-opik-test-env',
client_payload: {
pr_number: '${{ github.event.number }}',
version_name: process.env.VERSION_NAME,
is_base_version: false
}
});
console.log('✅ Repository dispatch sent successfully:', response.status);
console.log('📦 Deploying new version:', process.env.VERSION_NAME);
console.log('🔄 This will update the existing environment');
console.log('📊 Monitor deployment progress: https://github.com/comet-ml/comet-deployment/actions/workflows/deploy_opik_adhoc_env.yaml');
console.log('🔗 Deployment will verify the version and post a comment on this PR when complete');
- name: Handle trigger failure
if: failure()
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.issues.createComment({
issue_number: Number('${{ github.event.number }}'),
owner: context.repo.owner,
repo: context.repo.repo,
body: `❌ **Failed to trigger deployment with new version**
Please check the [workflow logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for more details.`
});
@@ -0,0 +1,148 @@
name: TypeScript SDK Compatibility V1.x E2E Tests
run-name: "TypeScript SDK Compatibility V1 E2E Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
checks: write
pull-requests: write
packages: write
on:
workflow_dispatch:
pull_request:
paths:
- 'sdks/typescript/**'
- 'apps/opik-backend/**'
- '.github/workflows/typescript_sdk_compatibility_v1_e2e_tests.yml'
push:
branches:
- 'main'
paths:
- 'sdks/typescript/**'
- 'apps/opik-backend/**'
- '.github/workflows/typescript_sdk_compatibility_v1_e2e_tests.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
OPIK_URL_OVERRIDE: http://localhost:8080
OPIK_CONSOLE_LOGGING_LEVEL: DEBUG
jobs:
select-matrix:
name: Select Node version matrix
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
node_versions: ${{ steps.pick.outputs.versions }}
steps:
- uses: actions/checkout@v6
with:
sparse-checkout: .github/actions/select-e2e-matrix
sparse-checkout-cone-mode: false
- uses: ./.github/actions/select-e2e-matrix
id: pick
with:
all-versions: ${{ vars.NODE_VERSIONS }}
match-regex: '^sdks/typescript/|^\.github/workflows/typescript_sdk_compatibility_v1_e2e_tests\.yml$'
github-token: ${{ secrets.GITHUB_TOKEN }}
build-opik:
uses: ./.github/workflows/build_e2e_docker.yaml
run-compatibility-v1-e2e:
needs: [select-matrix, build-opik]
name: TypeScript SDK Compatibility V1.x E2E Tests Node ${{matrix.node_version}}
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
node_version: ${{ fromJSON(needs.select-matrix.outputs.node_versions) }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull Docker images from GHCR
env:
TAG: ${{ needs.build-opik.outputs.image_tag }}
run: |
docker pull "ghcr.io/comet-ml/opik/opik-backend:$TAG"
docker pull "ghcr.io/comet-ml/opik/opik-python-backend:$TAG"
docker tag "ghcr.io/comet-ml/opik/opik-backend:$TAG" ghcr.io/comet-ml/opik/opik-backend:latest
docker tag "ghcr.io/comet-ml/opik/opik-python-backend:$TAG" ghcr.io/comet-ml/opik/opik-python-backend:latest
- name: Set up Node.js ${{matrix.node_version}}
uses: actions/setup-node@v4.2.0
with:
node-version: ${{matrix.node_version}}
- name: Run latest Opik server
env:
OPIK_USAGE_REPORT_ENABLED: false
COMPOSE_BAKE: false
OPIK_BACKEND_PULL_POLICY: never
PYTHON_BACKEND_PULL_POLICY: never
run: |
cd ${{ github.workspace }}
./opik.sh --backend --port-mapping
- name: Check Opik server availability
shell: bash
run: |
chmod +x ${{ github.workspace }}/tests_end_to_end/installer_utils/*.sh
cd ${{ github.workspace }}/deployment/docker-compose
echo "Check Docker pods are up"
${{ github.workspace }}/tests_end_to_end/installer_utils/check_docker_compose_pods.sh
echo "Check backend health"
${{ github.workspace }}/tests_end_to_end/installer_utils/check_backend.sh
- name: Install TypeScript SDK dependencies
working-directory: sdks/typescript
run: npm ci
- name: Build TypeScript SDK
working-directory: sdks/typescript
run: npm run build
- name: Run compatibility V1 tests
working-directory: sdks/typescript
run: |
npm run test:compatibility -- --reporter=default --reporter=junit --outputFile.junit=${{ github.workspace }}/compat_v1_results_node${{matrix.node_version}}.xml
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action/linux@v2
if: always()
with:
action_fail: true
comment_mode: failures
check_name: "TS SDK Compat V1 E2E Tests - Node ${{matrix.node_version}}"
files: ${{ github.workspace }}/compat_v1_results_node${{matrix.node_version}}.xml
- name: Keep BE log in case of failure
if: failure()
run: |
PROJECT_NAME=$(docker compose ls --format json 2>/dev/null | jq -r '.[].Name' | grep '^opik' | head -1)
CONTAINER="${PROJECT_NAME:-opik}-backend-1"
docker logs "$CONTAINER" > ${{ github.workspace }}/opik-backend_compat_v1_node${{matrix.node_version}}.log 2>&1 || echo "Could not capture logs from $CONTAINER"
- name: Attach BE log
if: failure()
uses: actions/upload-artifact@v7
with:
name: opik-backend-compat-v1-log-node${{matrix.node_version}}
path: ${{ github.workspace }}/opik-backend_compat_v1_node${{matrix.node_version}}.log
- name: Stop opik server
if: always()
run: |
cd ${{ github.workspace }}
./opik.sh --stop
@@ -0,0 +1,161 @@
name: TypeScript SDK E2E Tests
run-name: "TypeScript SDK E2E Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
checks: write
pull-requests: write
packages: write
on:
workflow_dispatch:
pull_request:
paths:
- 'sdks/typescript/**'
- 'apps/opik-backend/**'
- '.github/workflows/typescript_sdk_e2e_tests.yml'
push:
branches:
- 'main'
paths:
- 'sdks/typescript/**'
- 'apps/opik-backend/**'
- '.github/workflows/typescript_sdk_e2e_tests.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
OPIK_ENABLE_LITELLM_MODELS_MONITORING: False
OPIK_SENTRY_ENABLE: False
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_ID: ${{ secrets.OPENAI_ORG_ID }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPIK_URL_OVERRIDE: http://localhost:8080
OPIK_CONSOLE_LOGGING_LEVEL: DEBUG
jobs:
select-matrix:
name: Select Node version matrix
permissions:
contents: read
pull-requests: read
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
node_versions: ${{ steps.pick.outputs.versions }}
steps:
- uses: actions/checkout@v6
with:
sparse-checkout: .github/actions/select-e2e-matrix
sparse-checkout-cone-mode: false
- uses: ./.github/actions/select-e2e-matrix
id: pick
with:
all-versions: ${{ vars.NODE_VERSIONS }}
match-regex: '^sdks/typescript/|^\.github/workflows/typescript_sdk_e2e_tests\.yml$'
github-token: ${{ secrets.GITHUB_TOKEN }}
build-opik:
uses: ./.github/workflows/build_e2e_docker.yaml
run-e2e:
needs: [select-matrix, build-opik]
name: TypeScript SDK E2E Tests Node ${{matrix.node_version}}
permissions:
contents: read
checks: write
pull-requests: write
packages: read
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
node_version: ${{ fromJSON(needs.select-matrix.outputs.node_versions) }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull Docker images from GHCR
env:
TAG: ${{ needs.build-opik.outputs.image_tag }}
run: |
docker pull "ghcr.io/comet-ml/opik/opik-backend:$TAG"
docker pull "ghcr.io/comet-ml/opik/opik-python-backend:$TAG"
docker tag "ghcr.io/comet-ml/opik/opik-backend:$TAG" ghcr.io/comet-ml/opik/opik-backend:latest
docker tag "ghcr.io/comet-ml/opik/opik-python-backend:$TAG" ghcr.io/comet-ml/opik/opik-python-backend:latest
- name: Set up Node.js ${{matrix.node_version}}
uses: actions/setup-node@v4.2.0
with:
node-version: ${{matrix.node_version}}
- name: Run latest Opik server
env:
OPIK_USAGE_REPORT_ENABLED: false
COMPOSE_BAKE: false
TOGGLE_RUNNERS_ENABLED: "true"
OPIK_BACKEND_PULL_POLICY: never
PYTHON_BACKEND_PULL_POLICY: never
run: |
cd ${{ github.workspace }}
./opik.sh --backend --port-mapping
- name: Check Opik server availability
shell: bash
run: |
chmod +x ${{ github.workspace }}/tests_end_to_end/installer_utils/*.sh
cd ${{ github.workspace }}/deployment/docker-compose
echo "Check Docker pods are up"
${{ github.workspace }}/tests_end_to_end/installer_utils/check_docker_compose_pods.sh
echo "Check backend health"
${{ github.workspace }}/tests_end_to_end/installer_utils/check_backend.sh
- name: Install TypeScript SDK dependencies
working-directory: sdks/typescript
run: npm ci
- name: Build TypeScript SDK
working-directory: sdks/typescript
run: npm run build
- name: Run integration tests
working-directory: sdks/typescript
run: |
# once OPIK-3910 is fixed we can revert to npm run test:integration
npm run test:integration -- --reporter=default --reporter=junit --outputFile.junit=${{ github.workspace }}/test_results_node${{matrix.node_version}}.xml
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action/linux@v2
if: always()
with:
action_fail: true
comment_mode: failures
check_name: "TS SDK E2E Tests - Node ${{matrix.node_version}}"
files: ${{ github.workspace }}/test_results_node${{matrix.node_version}}.xml
- name: Keep BE log in case of failure
if: failure()
run: |
PROJECT_NAME=$(docker compose ls --format json 2>/dev/null | jq -r '.[].Name' | grep '^opik' | head -1)
CONTAINER="${PROJECT_NAME:-opik}-backend-1"
docker logs "$CONTAINER" > ${{ github.workspace }}/opik-backend_node${{matrix.node_version}}.log 2>&1 || echo "Could not capture logs from $CONTAINER"
- name: Attach BE log
if: failure()
uses: actions/upload-artifact@v7
with:
name: opik-backend-log-node${{matrix.node_version}}
path: ${{ github.workspace }}/opik-backend_node${{matrix.node_version}}.log
- name: Stop opik server
if: always()
run: |
cd ${{ github.workspace }}
./opik.sh --stop
@@ -0,0 +1,132 @@
name: TypeScript SDK Integration Build & Publish
run-name: "TypeScript SDK Integration Build & Publish ${{ github.event.inputs.version || inputs.version || 'auto' }} from ${{github.ref_name}} by @${{ github.actor }}"
on:
push:
paths:
- "sdks/typescript/src/opik/integrations/**"
- ".github/workflows/typescript_sdk_integration_publish.yml"
pull_request:
paths:
- "sdks/typescript/src/opik/integrations/**"
- ".github/workflows/typescript_sdk_integration_publish.yml"
workflow_dispatch:
inputs:
version:
type: string
required: true
description: Version
default: ""
is_release:
type: boolean
required: false
description: Whether this is a release (publishes to NPM)
default: false
skip_commit_push:
type: boolean
required: false
description: Skip commit and push
default: false
workflow_call:
inputs:
version:
type: string
required: true
description: Version
is_release:
type: boolean
required: false
default: false
skip_commit_push:
type: boolean
required: false
default: false
concurrency:
group: ts-sdk-integration-publish-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build-and-publish:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
integration:
- opik-openai
- opik-gemini
- opik-langchain
- opik-vercel
- opik-otel
env:
IS_RELEASE: ${{ inputs.is_release }}
INTEGRATION: ${{ matrix.integration }}
WORKING_DIR: sdks/typescript/src/opik/integrations/${{ matrix.integration }}
defaults:
run:
working-directory: ${{ env.WORKING_DIR }}
steps:
- name: Checkout
# For releases, check out the tag for the version we're publishing — not
# `github.ref` (which is the dispatch ref and may have moved past the tag).
# Convention: tag name == version string. Locks the build to the commit
# the tag points to, so the published tarball is reproducible from the
# version label. Non-release runs (build-only PR / push triggers) keep
# the dispatch ref because there's no tag to pin to.
#
# NOTE: This is duplicated with typescript_sdk_publish.yml. We tried
# extracting into a local composite action but a local composite cannot
# be a workflow's first step — the repo isn't checked out yet, so the
# composite's action.yml is not on the runner's filesystem. Keep the
# checkout inline.
uses: actions/checkout@v6
with:
ref: ${{ env.IS_RELEASE == 'true' && format('refs/tags/{0}', inputs.version) || github.ref }}
fetch-depth: 0
token: ${{ secrets.GH_PAT_TO_ACCESS_GITHUB_API || github.token }}
- name: Verify integration exists
run: |
if [ ! -d "${{ github.workspace }}/${{ env.WORKING_DIR }}" ]; then
echo "Error: Integration '${{ env.INTEGRATION }}' not found at path: ${{ env.WORKING_DIR }}"
exit 1
fi
echo "Integration '${{ env.INTEGRATION }}' found"
- name: Build and Publish TypeScript SDK Integration
id: build_publish
uses: ./.github/actions/typescript-sdk-build-and-publish
with:
working_directory: ${{ env.WORKING_DIR }}
version: ${{ inputs.version || '' }}
is_release: ${{ env.IS_RELEASE }}
package_name: ${{ env.INTEGRATION }}
npm_access: "public"
enable_cache: "true"
cache_dependency_path: ${{ env.WORKING_DIR }}/package-lock.json
test_package_installation: "true"
npm_token: ${{ secrets.NPM_TOKEN }}
github_token: ${{ secrets.GH_PAT_TO_ACCESS_GITHUB_API || github.token }}
skip_commit_push: ${{ inputs.skip_commit_push }}
- name: Summary
run: |
exec >> "$GITHUB_STEP_SUMMARY"
echo "## ${{ env.INTEGRATION }} Build"
echo ""
echo "**Version:** ${{ steps.build_publish.outputs.version }}"
echo "**Event:** ${{ github.event_name }}"
echo "**Is Release:** ${{ env.IS_RELEASE }}"
if [ "${{ env.IS_RELEASE }}" = "true" ]; then
echo "**Published:** ✅ Published to NPM"
echo "**Package:** 📦 ${{ steps.build_publish.outputs.package_name }}@${{ steps.build_publish.outputs.version }}"
echo "**NPM:** 🔗 https://www.npmjs.com/package/${{ steps.build_publish.outputs.package_name }}/v/${{ steps.build_publish.outputs.version }}"
echo ""
echo "**Install with:**"
echo "\`\`\`bash"
echo "npm install ${{ steps.build_publish.outputs.package_name }}@${{ steps.build_publish.outputs.version }}"
echo "\`\`\`"
else
echo "**Status:** ⏭️ Build and tests only"
fi
@@ -0,0 +1,69 @@
# Real library integration tests for the TypeScript SDK.
#
# Mirrors the Python `lib-integration-tests-runner.yml`: runs the integration
# packages' test suites on TypeScript SDK PRs, with provider API keys supplied
# from secrets so the gated live-LLM tests execute. Deterministic tests in the
# same packages run regardless; the live tests skip themselves when their key
# is absent (e.g. fork PRs), so the job stays green either way.
name: TypeScript SDK Library Integration Tests
run-name: "TypeScript SDK Library Integration Tests ${{ github.ref_name }} by @${{ github.actor }}"
permissions:
contents: read
on:
pull_request:
paths:
- "sdks/typescript/**"
push:
branches:
- "main"
paths:
- "sdks/typescript/**"
workflow_dispatch:
schedule:
# Daily at midnight UTC, Monday-Saturday.
- cron: "0 0 * * 1-6"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
jobs:
check_secrets:
name: Check Secrets
runs-on: ubuntu-latest
outputs:
has_anthropic_key: ${{ steps.check.outputs.has_anthropic_key }}
steps:
- name: Detect available provider keys
id: check
run: echo "has_anthropic_key=${{ secrets.ANTHROPIC_API_KEY != '' }}" >> "$GITHUB_OUTPUT"
vercel:
name: opik-vercel (Vercel AI SDK / eve)
needs: [check_secrets]
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: sdks/typescript/src/opik/integrations/opik-vercel
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v4.2.0
with:
node-version: "22"
- name: Warn when ANTHROPIC_API_KEY is absent
if: ${{ needs.check_secrets.outputs.has_anthropic_key == 'false' }}
run: echo "::warning::ANTHROPIC_API_KEY is not set - live LLM tests will be skipped (deterministic tests still run)."
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
@@ -0,0 +1,105 @@
name: TypeScript SDK Build & Publish
run-name: "TypeScript SDK Build & Publish ${{ github.event.inputs.version || inputs.version || 'auto' }} from ${{github.ref_name}} by @${{ github.actor }}"
on:
push:
branches:
- main
paths:
- "sdks/typescript/**"
- ".github/workflows/typescript_sdk_publish.yml"
pull_request:
paths:
- "sdks/typescript/**"
- ".github/workflows/typescript_sdk_publish.yml"
workflow_dispatch:
inputs:
version:
type: string
required: true
description: Version
default: ""
is_release:
type: boolean
required: false
description: Whether this is a release (publishes to NPM)
default: false
skip_commit_push:
type: boolean
required: false
description: Skip commit and push
default: false
workflow_call:
inputs:
version:
type: string
required: true
description: Version
is_release:
type: boolean
required: false
default: false
skip_commit_push:
type: boolean
required: false
default: false
concurrency:
group: ts-sdk-publish-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build-and-publish:
runs-on: ubuntu-latest
timeout-minutes: 5
env:
IS_RELEASE: ${{ inputs.is_release }}
defaults:
run:
working-directory: sdks/typescript
steps:
- name: Checkout
# For releases, check out the tag for the version we're publishing — not
# `github.ref` (which is the dispatch ref and may have moved past the tag).
# Convention: tag name == version string. Locks the build to the commit
# the tag points to, so the published tarball is reproducible from the
# version label. Non-release runs (build-only PR / push triggers) keep
# the dispatch ref because there's no tag to pin to.
#
# NOTE: We extract this into a local composite action originally, but a
# local composite cannot be a workflow's first step — the repo isn't
# checked out yet, so the composite's action.yml is not on the runner's
# filesystem. Keep the checkout inline. See the closed comment thread on
# PR #7124 for the failure mode.
uses: actions/checkout@v6
with:
ref: ${{ env.IS_RELEASE == 'true' && format('refs/tags/{0}', inputs.version) || github.ref }}
fetch-depth: 0
token: ${{ secrets.GH_PAT_TO_ACCESS_GITHUB_API || github.token }}
- name: Build and Publish TypeScript SDK
id: build_publish
uses: ./.github/actions/typescript-sdk-build-and-publish
with:
working_directory: sdks/typescript
version: ${{ inputs.version || '' }}
is_release: ${{ env.IS_RELEASE }}
package_name: "TypeScript SDK"
npm_token: ${{ secrets.NPM_TOKEN }}
github_token: ${{ secrets.GH_PAT_TO_ACCESS_GITHUB_API || github.token }}
skip_commit_push: ${{ inputs.skip_commit_push }}
- name: Summary
run: |
exec >> "$GITHUB_STEP_SUMMARY"
echo "## TypeScript SDK Build"
echo ""
echo "**Version:** ${{ steps.build_publish.outputs.version }}"
echo "**Event:** ${{ github.event_name }}"
echo "**Is Release:** ${{ env.IS_RELEASE }}"
if [ "${{ env.IS_RELEASE }}" = "true" ]; then
echo "**Published:** ✅ Published to NPM"
else
echo " ⏭️ Build only "
fi
@@ -0,0 +1,52 @@
name: TypeScript SDK Unit Tests
run-name: "TypeScript SDK Unit Tests ${{ github.ref_name }} by @${{ github.actor }}"
on:
pull_request:
paths:
- "sdks/typescript/**"
push:
branches:
- "main"
paths:
- "sdks/typescript/**"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
test:
name: Test on Node ${{ matrix.node-version }}
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: sdks/typescript
strategy:
fail-fast: false
matrix:
node-version: ${{ fromJSON(vars.NODE_VERSIONS || '[18,20,22]') }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v4.2.0
with:
node-version: ${{ matrix.node-version || '20' }}
- name: Install dependencies (npm)
run: npm ci
- name: Run tests (npm)
run: npm run test
- name: Install 'configure' tool dependencies
working-directory: sdks/typescript/src/opik/configure
run: npm install
- name: Run 'configure' tool tests
working-directory: sdks/typescript/src/opik/configure
run: npm run test
+50
View File
@@ -0,0 +1,50 @@
name: Generate helm docs
run-name: Generate helm docs ${{ github.ref_name }} by @${{ github.actor }}"
on:
pull_request:
paths:
- "deployment/helm_chart/opik/**"
types: [opened, synchronize, reopened, labeled]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
update-readme:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.ref }}
# If the PR has the "update readme" label, generate and commit
- name: Generate and commit helm docs
if: contains(github.event.pull_request.labels.*.name, 'update readme')
uses: losisin/helm-docs-github-action@v1
with:
chart-search-root: deployment/helm_chart/opik
git-push: true
git-push-user-name: "CometActions"
git-push-user-email: "github-actions@comet.com"
git-commit-message: "Update Helm documentation"
# If the PR does NOT have the label, just validate that docs are up-to-date
- name: Validate helm docs are up-to-date
if: "!contains(github.event.pull_request.labels.*.name, 'update readme')"
uses: losisin/helm-docs-github-action@v1
with:
chart-search-root: deployment/helm_chart/opik
git-push: false
- name: Check for uncommitted helm docs changes
if: "!contains(github.event.pull_request.labels.*.name, 'update readme')"
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "::error::Helm docs are out of date. Please run 'pre-commit run helm-docs --all-files' locally and commit the changes, or add the 'update readme' label to this PR and don't forget to update your local branch afterwards."
git status --short
exit 1
fi