chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# .github/workflows/assign-reviewers.yml
|
||||
name: Auto-assign reviewers
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
assign:
|
||||
if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.fork == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
github-token: ${{ secrets.REVIEW_TOKEN }}
|
||||
script: |
|
||||
await github.rest.pulls.requestReviewers({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
team_reviewers: ['agent-devs']
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
name: Build package
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
package:
|
||||
required: true
|
||||
type: string
|
||||
artifact_name:
|
||||
required: true
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: "Name of the package to build"
|
||||
required: true
|
||||
default: "livekit-agents"
|
||||
artifact_name:
|
||||
description: "Artifact name for the distribution package"
|
||||
required: true
|
||||
default: "build-artifact"
|
||||
|
||||
jobs:
|
||||
build_plugins:
|
||||
runs-on: ubuntu-latest
|
||||
if: inputs.package != 'livekit-blockguard' && inputs.package != 'livekit-durable'
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
submodules: true
|
||||
lfs: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- name: Build package
|
||||
run: uv build --package ${{inputs.package}}
|
||||
|
||||
- name: Upload distribution package
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}
|
||||
path: dist/
|
||||
|
||||
build_durable:
|
||||
if: inputs.package == 'livekit-durable'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
archs: x86_64
|
||||
- os: namespace-profile-default-arm64
|
||||
archs: aarch64
|
||||
- os: windows-latest
|
||||
archs: AMD64
|
||||
- os: macos-latest
|
||||
archs: x86_64 arm64
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: livekit-plugins/livekit-durable
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install cibuildwheel
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install cibuildwheel
|
||||
|
||||
- name: Build wheels
|
||||
run: cibuildwheel --output-dir dist
|
||||
env:
|
||||
CIBW_BUILD_VERBOSITY: 3
|
||||
CIBW_ARCHS: ${{ matrix.archs }}
|
||||
|
||||
- name: Upload distribution package
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}-${{ matrix.os }}
|
||||
path: livekit-plugins/livekit-durable/dist/
|
||||
|
||||
build_blockguard:
|
||||
if: inputs.package == 'livekit-blockguard'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
archs: x86_64
|
||||
- os: namespace-profile-default-arm64
|
||||
archs: aarch64
|
||||
- os: windows-latest
|
||||
archs: AMD64
|
||||
- os: macos-latest
|
||||
archs: x86_64 arm64
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: livekit-plugins/livekit-blockguard
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install cibuildwheel
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install cibuildwheel
|
||||
|
||||
- name: Build wheels
|
||||
run: cibuildwheel --output-dir dist
|
||||
env:
|
||||
CIBW_BUILD_VERBOSITY: 3
|
||||
CIBW_ARCHS: ${{ matrix.archs }}
|
||||
|
||||
- name: Upload distribution package
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}-${{ matrix.os }}
|
||||
path: livekit-plugins/livekit-blockguard/dist/
|
||||
|
||||
merge_artifacts:
|
||||
if: ${{ always() && (inputs.package == 'livekit-blockguard' || inputs.package == 'livekit-durable') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build_blockguard, build_durable]
|
||||
steps:
|
||||
- name: Download all platform wheels
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
pattern: ${{ inputs.artifact_name }}-*
|
||||
path: all_dists
|
||||
merge-multiple: true
|
||||
|
||||
- run: ls -R all_dists
|
||||
|
||||
- name: Upload unified wheel artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}
|
||||
path: all_dists/
|
||||
@@ -0,0 +1,78 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, 0.x]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ruff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install the project
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- name: Ruff
|
||||
run: uv run ruff check --output-format=github .
|
||||
continue-on-error: true
|
||||
id: ruff-check
|
||||
|
||||
- name: Check format
|
||||
run: uv run ruff format --check .
|
||||
continue-on-error: true
|
||||
id: ruff-format
|
||||
|
||||
- name: Report ruff errors
|
||||
if: steps.ruff-check.outcome == 'failure' || steps.ruff-format.outcome == 'failure'
|
||||
run: |
|
||||
echo "::error::Ruff checks failed. Please run 'make fix' locally to automatically fix formatting and linting issues."
|
||||
exit 1
|
||||
|
||||
type-check:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install the project
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- name: Cache mypy
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: .mypy_cache
|
||||
key: mypy-${{ matrix.python-version }}-${{ hashFiles('uv.lock') }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
mypy-${{ matrix.python-version }}-${{ hashFiles('uv.lock') }}-
|
||||
mypy-${{ matrix.python-version }}-
|
||||
|
||||
- name: Check Types
|
||||
run: uv run python scripts/check_types.py
|
||||
@@ -0,0 +1,168 @@
|
||||
name: Deploy Examples
|
||||
|
||||
on:
|
||||
# Manual runs deploy from the branch picked in the Actions "Use workflow
|
||||
# from" selector — no input needed.
|
||||
workflow_dispatch:
|
||||
# Called by publish.yml after a release to deploy the published commit.
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: "Commit/branch/tag to deploy. Defaults to the triggering ref."
|
||||
type: string
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy ${{ matrix.example }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
example: [healthcare, survey, frontdesk, drive-thru, inference, avatar, hotel_receptionist]
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
# Fetch Git-LFS assets (e.g. examples/drive-thru/bg_noise.mp3) so the
|
||||
# real binaries — not pointer files — get uploaded as the build context
|
||||
# and copied into the image via `COPY . .`. This checkout authenticates
|
||||
# with GITHUB_TOKEN, so its LFS fetch is allowed (unlike pip's anonymous
|
||||
# git clone). Without this the agent crashes at runtime decoding a
|
||||
# 131-byte pointer as audio.
|
||||
lfs: true
|
||||
|
||||
- name: Install LiveKit CLI
|
||||
run: |
|
||||
curl -sSL https://get.livekit.io/cli | bash
|
||||
lk --version
|
||||
|
||||
- name: Add LiveKit Cloud project
|
||||
env:
|
||||
LIVEKIT_URL: ${{ secrets.LIVEKIT_EXAMPLES_URL }}
|
||||
LIVEKIT_API_KEY: ${{ secrets.LIVEKIT_EXAMPLES_API_KEY }}
|
||||
LIVEKIT_API_SECRET: ${{ secrets.LIVEKIT_EXAMPLES_API_SECRET }}
|
||||
run: |
|
||||
lk project add examples \
|
||||
--url "$LIVEKIT_URL" \
|
||||
--api-key "$LIVEKIT_API_KEY" \
|
||||
--api-secret "$LIVEKIT_API_SECRET" \
|
||||
--default
|
||||
|
||||
- name: Regenerate livekit.toml from playground.yaml
|
||||
run: |
|
||||
python3 -m pip install --quiet pyyaml
|
||||
python3 <<'PY'
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
data = yaml.safe_load(Path("examples/playground.yaml").read_text())
|
||||
subdomain = data["project"]["subdomain"]
|
||||
slug = "${{ matrix.example }}"
|
||||
entry = data["examples"][slug]
|
||||
toml = (
|
||||
"[project]\n"
|
||||
f' subdomain = "{subdomain}"\n'
|
||||
"\n"
|
||||
"[agent]\n"
|
||||
f' id = "{entry["agent_id"]}"\n'
|
||||
)
|
||||
Path(f"examples/{slug}/livekit.toml").write_text(toml)
|
||||
print(f"wrote examples/{slug}/livekit.toml")
|
||||
PY
|
||||
|
||||
- name: Pin livekit-* requirements to the deployed ref
|
||||
working-directory: examples/${{ matrix.example }}
|
||||
env:
|
||||
# Build the agent against the code at the ref we're deploying,
|
||||
# not the latest release on PyPI. Without this, a branch deploy
|
||||
# would silently run against the published livekit-agents instead
|
||||
# of the branch's own SDK changes. Mirror the checkout ref above,
|
||||
# using ref_name so it's a plain branch/tag pip can resolve.
|
||||
DEPLOY_REF: ${{ inputs.ref || github.ref_name }}
|
||||
run: |
|
||||
python3 <<'PY'
|
||||
import os, re
|
||||
from pathlib import Path
|
||||
|
||||
REF = os.environ["DEPLOY_REF"]
|
||||
REPO = "git+https://github.com/livekit/agents.git"
|
||||
REPO_ROOT = Path(os.environ["GITHUB_WORKSPACE"])
|
||||
|
||||
# The package name (and optional [extras]) at the start of a
|
||||
# requirement, e.g. "livekit-agents[evals]>=1.5.7".
|
||||
REQUIREMENT = re.compile(r"^(?P<name>[A-Za-z0-9._-]+)(?P<extras>\[.*\])?")
|
||||
|
||||
def monorepo_path(name):
|
||||
"""Path of `name` within this repo, or None if it lives elsewhere.
|
||||
|
||||
We only pin packages that actually exist in the checkout. The
|
||||
directory basename is the distribution name for every package
|
||||
here (livekit-agents at the root, everything else under
|
||||
livekit-plugins/). Anything not found (livekit rtc,
|
||||
livekit-blingfire, livekit-local-inference, …) keeps its
|
||||
PyPI pin.
|
||||
"""
|
||||
for rel in (name, f"livekit-plugins/{name}"):
|
||||
if (REPO_ROOT / rel).is_dir():
|
||||
return rel
|
||||
return None
|
||||
|
||||
def pin_to_ref(line):
|
||||
"""Repoint an in-repo requirement at the deployed git ref.
|
||||
|
||||
External requirements, comments and blanks pass through
|
||||
untouched (the regex doesn't match a leading '#' or '').
|
||||
"""
|
||||
match = REQUIREMENT.match(line.strip())
|
||||
path = monorepo_path(match["name"]) if match else None
|
||||
if path is None:
|
||||
return line
|
||||
return f"{match['name']}{match['extras'] or ''} @ {REPO}@{REF}#subdirectory={path}"
|
||||
|
||||
requirements = Path("requirements.txt")
|
||||
pinned = [pin_to_ref(line) for line in requirements.read_text().splitlines()]
|
||||
requirements.write_text("\n".join(pinned) + "\n")
|
||||
|
||||
print(f"--- requirements.txt pinned to {REF} ---")
|
||||
print(requirements.read_text())
|
||||
PY
|
||||
|
||||
- name: Build secrets file
|
||||
working-directory: examples/${{ matrix.example }}
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
LEMONSLICE_API_KEY: ${{ secrets.LEMONSLICE_API_KEY }}
|
||||
run: |
|
||||
: > .env.deploy
|
||||
# Register the agent under a deterministic name so the playground
|
||||
# can dispatch to it explicitly. Matches the slug in playground.yaml.
|
||||
echo "LIVEKIT_AGENT_NAME=${{ matrix.example }}" >> .env.deploy
|
||||
case "${{ matrix.example }}" in
|
||||
healthcare)
|
||||
keys="OPENAI_API_KEY"
|
||||
;;
|
||||
avatar)
|
||||
keys="LEMONSLICE_API_KEY"
|
||||
;;
|
||||
*)
|
||||
keys=""
|
||||
;;
|
||||
esac
|
||||
for k in $keys; do
|
||||
val="${!k}"
|
||||
if [ -n "$val" ]; then
|
||||
echo "${k}=${val}" >> .env.deploy
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Deploy ${{ matrix.example }}
|
||||
working-directory: examples/${{ matrix.example }}
|
||||
run: |
|
||||
args=()
|
||||
if [ -s .env.deploy ]; then
|
||||
args+=(--secrets-file .env.deploy)
|
||||
fi
|
||||
lk agent deploy "${args[@]}" .
|
||||
@@ -0,0 +1,83 @@
|
||||
name: Weekly Python Download Stats
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 14 * * 1" # every Monday at 2pm UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
stats:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Generate report
|
||||
run: python .github/download_stats.py > stats.txt
|
||||
|
||||
- name: Post to Slack
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
CHANNEL: ${{ vars.DOWNLOAD_STATS_SLACK_CHANNEL }}
|
||||
run: |
|
||||
python3 << 'PYEOF'
|
||||
import json, os, urllib.request
|
||||
from datetime import datetime
|
||||
|
||||
token = os.environ["SLACK_BOT_TOKEN"]
|
||||
channel = os.environ["CHANNEL"]
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
def slack_api(method, payload):
|
||||
req = urllib.request.Request(
|
||||
f"https://slack.com/api/{method}",
|
||||
data=json.dumps(payload).encode() if isinstance(payload, dict) else payload,
|
||||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
if isinstance(payload, dict) else {"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
return json.loads(urllib.request.urlopen(req).read())
|
||||
|
||||
# 1. Post summary message
|
||||
resp = slack_api("chat.postMessage", {
|
||||
"channel": channel,
|
||||
"text": f":python: *Weekly Python PyPI Download Stats* — {today}",
|
||||
})
|
||||
assert resp["ok"], f"chat.postMessage failed: {resp.get('error')}"
|
||||
thread_ts = resp["ts"]
|
||||
|
||||
# 2. Get upload URL
|
||||
file_size = os.path.getsize("stats.txt")
|
||||
form_data = f"filename=pypi-stats-{today}.txt&length={file_size}".encode()
|
||||
req = urllib.request.Request(
|
||||
"https://slack.com/api/files.getUploadURLExternal",
|
||||
data=form_data,
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
)
|
||||
resp = json.loads(urllib.request.urlopen(req).read())
|
||||
assert resp["ok"], f"getUploadURLExternal failed: {resp.get('error')}"
|
||||
upload_url = resp["upload_url"]
|
||||
file_id = resp["file_id"]
|
||||
|
||||
# 3. Upload file content
|
||||
import subprocess
|
||||
subprocess.run(["curl", "-s", "-X", "POST", upload_url, "-F", "file=@stats.txt"], check=True)
|
||||
|
||||
# 4. Complete upload in thread
|
||||
resp = slack_api("files.completeUploadExternal", {
|
||||
"files": [{"id": file_id, "title": f"pypi-stats-{today}.txt"}],
|
||||
"channel_id": channel,
|
||||
"thread_ts": thread_ts,
|
||||
"initial_comment": "Full report",
|
||||
})
|
||||
assert resp["ok"], f"completeUploadExternal failed: {resp.get('error')}"
|
||||
print("Posted to Slack successfully")
|
||||
PYEOF
|
||||
@@ -0,0 +1,83 @@
|
||||
name: evals
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
evals:
|
||||
# don't run tests for PRs on forks
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
example: [frontdesk, drive-thru]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
name: ${{ matrix.example }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install the project
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- name: Setup Google credentials
|
||||
if: matrix.example == 'google'
|
||||
shell: bash
|
||||
run: |
|
||||
printf '%s' '${{ secrets.GOOGLE_CREDENTIALS_JSON }}' > tests/google.json
|
||||
|
||||
- name: Run tests
|
||||
shell: bash
|
||||
env:
|
||||
LIVEKIT_EVALS_VERBOSE: 1
|
||||
PLUGIN: ${{ matrix.example }}
|
||||
LIVEKIT_URL: ${{ secrets.LIVEKIT_URL }}
|
||||
LIVEKIT_API_KEY: ${{ secrets.LIVEKIT_API_KEY }}
|
||||
LIVEKIT_API_SECRET: ${{ secrets.LIVEKIT_API_SECRET }}
|
||||
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
ELEVEN_API_KEY: ${{ secrets.ELEVEN_API_KEY }}
|
||||
CARTESIA_API_KEY: ${{ secrets.CARTESIA_API_KEY }}
|
||||
AZURE_SPEECH_KEY: ${{ secrets.AZURE_SPEECH_KEY }}
|
||||
AZURE_SPEECH_REGION: ${{ secrets.AZURE_SPEECH_REGION }} # nit: doesn't have to be secret
|
||||
GOOGLE_CREDENTIALS_JSON: ${{ secrets.GOOGLE_CREDENTIALS_JSON }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
ASSEMBLYAI_API_KEY: ${{ secrets.ASSEMBLYAI_API_KEY }}
|
||||
FAL_KEY: ${{ secrets.FAL_KEY }}
|
||||
PLAYHT_API_KEY: ${{ secrets.PLAYHT_API_KEY }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
PLAYHT_USER_ID: ${{ secrets.PLAYHT_USER_ID }}
|
||||
RIME_API_KEY: ${{ secrets.RIME_API_KEY }}
|
||||
SPEECHMATICS_API_KEY: ${{ secrets.SPEECHMATICS_API_KEY }}
|
||||
GOOGLE_APPLICATION_CREDENTIALS: tests/google.json
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
NEUPHONIC_API_KEY: ${{ secrets.NEUPHONIC_API_KEY }}
|
||||
RESEMBLE_API_KEY: ${{ secrets.RESEMBLE_API_KEY }}
|
||||
SPEECHIFY_API_KEY: ${{ secrets.SPEECHIFY_API_KEY }}
|
||||
HUME_API_KEY: ${{ secrets.HUME_API_KEY }}
|
||||
SPITCH_API_KEY: ${{ secrets.SPITCH_API_KEY }}
|
||||
LMNT_API_KEY: ${{ secrets.LMNT_API_KEY }}
|
||||
INWORLD_API_KEY: ${{ secrets.INWORLD_API_KEY }}
|
||||
run: uv run pytest examples/${{matrix.example}}/test_agent.py -s
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Publish docs
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
secrets:
|
||||
DOCS_DEPLOY_AWS_ACCESS_KEY: {}
|
||||
DOCS_DEPLOY_AWS_API_SECRET: {}
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
submodules: true
|
||||
lfs: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- name: Install package, plugins, and docs dependencies
|
||||
run: |
|
||||
uv sync --all-extras --group docs
|
||||
|
||||
- name: Build HTML Docs
|
||||
run: |
|
||||
uv run --active pdoc --skip-errors --html --output-dir=docs livekit
|
||||
|
||||
- name: Build Markdown Docs
|
||||
run: |
|
||||
uv run --active python .github/convert_html_docs.py docs/ docs-md -v --check-links
|
||||
|
||||
- name: S3 Upload
|
||||
run: |
|
||||
aws s3 sync --delete docs/ s3://livekit-docs/python
|
||||
aws s3 sync --delete docs-md/ s3://livekit-docs/python-md
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.DOCS_DEPLOY_AWS_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DOCS_DEPLOY_AWS_API_SECRET }}
|
||||
AWS_DEFAULT_REGION: "us-east-1"
|
||||
|
||||
- name: Expire cloudfront cache
|
||||
run: |
|
||||
aws cloudfront create-invalidation --distribution-id EJJ40KLJ3TRY9 --paths "/python/*" "/python-md/*"
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.DOCS_DEPLOY_AWS_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.DOCS_DEPLOY_AWS_API_SECRET }}
|
||||
AWS_DEFAULT_REGION: "us-east-1"
|
||||
@@ -0,0 +1,291 @@
|
||||
name: Publish to PyPI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "What to publish"
|
||||
type: choice
|
||||
required: true
|
||||
options:
|
||||
- "patch (1.5.1 → 1.5.2)"
|
||||
- "minor (1.5.1 → 1.6.0)"
|
||||
- "major (1.5.1 → 2.0.0)"
|
||||
- "patch-rc (1.5.1 → 1.5.2.rc1)"
|
||||
- "minor-rc (1.5.1 → 1.6.0.rc1)"
|
||||
- "major-rc (1.5.1 → 2.0.0.rc1)"
|
||||
- "next-rc (.rc1 → .rc2)"
|
||||
- "promote (1.6.0.rc2 → 1.6.0)"
|
||||
- "republish (retry failed publish, no version bump)"
|
||||
branch:
|
||||
description: "Branch to publish from (default: main)"
|
||||
type: string
|
||||
required: false
|
||||
default: "main"
|
||||
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
# ── Create a version bump PR ────────────────────────────────
|
||||
bump:
|
||||
name: Create release PR
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch'
|
||||
&& !startsWith(inputs.version, 'republish')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
ref: ${{ inputs.branch }}
|
||||
|
||||
- name: Guard non-main branches
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
BRANCH: ${{ inputs.branch }}
|
||||
run: |
|
||||
key=$(echo "$VERSION" | awk '{print $1}')
|
||||
if [ "$BRANCH" != "main" ]; then
|
||||
case "$key" in
|
||||
*-rc|next-rc) ;; # allowed
|
||||
*) echo "::error::Only RC releases are allowed from non-main branches (got '$key' on '$BRANCH')"; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install click packaging pyyaml colorama
|
||||
|
||||
- name: Bump versions
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
key=$(echo "$VERSION" | awk '{print $1}')
|
||||
|
||||
case "$key" in
|
||||
patch|minor|major)
|
||||
python .github/update_versions.py --bump-type "$key"
|
||||
;;
|
||||
patch-rc|minor-rc|major-rc)
|
||||
bump="${key%-rc}"
|
||||
python .github/update_versions.py --bump-type "$bump"
|
||||
python .github/update_versions.py --pre rc
|
||||
;;
|
||||
next-rc)
|
||||
python .github/update_versions.py --pre rc
|
||||
;;
|
||||
promote)
|
||||
python .github/update_versions.py --bump-type release
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Read new version
|
||||
id: version
|
||||
run: |
|
||||
version=$(grep -m1 '__version__' livekit-agents/livekit/agents/version.py | sed 's/.*"\(.*\)".*/\1/')
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
echo "New version: $version"
|
||||
|
||||
- name: Close existing release PRs
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh pr list --state open --json number,headRefName \
|
||||
--jq '.[] | select(.headRefName | startswith("release/v")) | .number' | while read -r pr; do
|
||||
echo "Superseding release PR #$pr"
|
||||
gh pr comment "$pr" --body "Superseded by a new release."
|
||||
gh pr close "$pr" --delete-branch || true
|
||||
done
|
||||
|
||||
- name: Create release PR
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
BASE_BRANCH: ${{ inputs.branch }}
|
||||
run: |
|
||||
branch="release/v${VERSION}"
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git checkout -b "$branch"
|
||||
git add -A
|
||||
git commit -m "v${VERSION}"
|
||||
git push --force origin "$branch"
|
||||
|
||||
gh pr create \
|
||||
--base "$BASE_BRANCH" \
|
||||
--head "$branch" \
|
||||
--title "livekit-agents@${VERSION}" \
|
||||
--body "Merging this PR will publish all packages as **${VERSION}** to PyPI."
|
||||
|
||||
# ── Discover, build, publish ────────────────────────────────
|
||||
# Triggered by: merging a release PR OR republish dispatch
|
||||
discover:
|
||||
name: Discover packages
|
||||
if: |
|
||||
(github.event_name == 'pull_request'
|
||||
&& github.event.pull_request.merged == true
|
||||
&& startsWith(github.event.pull_request.head.ref, 'release/v'))
|
||||
|| (github.event_name == 'workflow_dispatch'
|
||||
&& startsWith(inputs.version, 'republish'))
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
packages: ${{ steps.list.outputs.packages }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
ref: ${{ inputs.branch || github.event.pull_request.merge_commit_sha }}
|
||||
|
||||
- name: List publishable packages
|
||||
id: list
|
||||
run: |
|
||||
packages=()
|
||||
|
||||
packages+=("livekit-agents")
|
||||
|
||||
for dir in livekit-plugins/livekit-plugins-*; do
|
||||
if [ ! -d "$dir" ]; then continue; fi
|
||||
|
||||
name=""
|
||||
if [ -f "$dir/pyproject.toml" ]; then
|
||||
name=$(grep -m1 '^name\s*=\s*"' "$dir/pyproject.toml" | sed 's/.*"\(.*\)".*/\1/')
|
||||
fi
|
||||
if [ -z "$name" ] && [ -f "$dir/setup.py" ]; then
|
||||
name=$(grep -m1 'name\s*=\s*"' "$dir/setup.py" | sed 's/.*"\(.*\)".*/\1/')
|
||||
fi
|
||||
if [ -z "$name" ]; then
|
||||
name=$(basename "$dir")
|
||||
fi
|
||||
|
||||
packages+=("$name")
|
||||
done
|
||||
|
||||
json=$(printf '%s\n' "${packages[@]}" | jq -R . | jq -sc .)
|
||||
echo "packages=$json" >> "$GITHUB_OUTPUT"
|
||||
echo "Will publish: $json"
|
||||
|
||||
tag:
|
||||
name: Tag release
|
||||
needs: discover
|
||||
if: |
|
||||
github.event_name == 'pull_request'
|
||||
&& github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
|
||||
- name: Create git tag
|
||||
env:
|
||||
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
run: |
|
||||
# HEAD_REF is release/v1.5.2 → extract 1.5.2
|
||||
version="${HEAD_REF#release/v}"
|
||||
tag="livekit-agents@${version}"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag "$tag"
|
||||
git push origin "$tag"
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.package }}
|
||||
needs: discover
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package: ${{ fromJson(needs.discover.outputs.packages) }}
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
package: ${{ matrix.package }}
|
||||
artifact_name: dist-${{ matrix.package }}
|
||||
|
||||
publish:
|
||||
name: Publish ${{ matrix.package }}
|
||||
needs: [discover, build]
|
||||
if: always() && needs.discover.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 10
|
||||
matrix:
|
||||
package: ${{ fromJson(needs.discover.outputs.packages) }}
|
||||
environment: pypi
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Download build artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
name: dist-${{ matrix.package }}
|
||||
path: dist/
|
||||
|
||||
- name: List distributions
|
||||
run: ls -la dist/
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
|
||||
|
||||
docs:
|
||||
name: Publish docs
|
||||
needs: [publish]
|
||||
if: always() && needs.publish.result == 'success'
|
||||
uses: ./.github/workflows/publish-docs.yml
|
||||
secrets: inherit
|
||||
|
||||
deploy-examples:
|
||||
name: Deploy examples
|
||||
needs: [publish]
|
||||
if: always() && needs.publish.result == 'success'
|
||||
uses: ./.github/workflows/deploy-examples.yml
|
||||
with:
|
||||
ref: ${{ inputs.branch || github.event.pull_request.merge_commit_sha }}
|
||||
secrets: inherit
|
||||
|
||||
dispatch-downstream-bumps:
|
||||
name: Bump livekit-agents in downstream repos
|
||||
needs: [publish]
|
||||
# Only fire for stable releases merged via a release/vX.Y.Z PR.
|
||||
# RC head refs look like release/v1.5.13rc1 and won't match the regex below.
|
||||
# Republish retries should use internal-actions' workflow_dispatch fallback.
|
||||
if: |
|
||||
always()
|
||||
&& needs.publish.result == 'success'
|
||||
&& github.event_name == 'pull_request'
|
||||
&& github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Resolve stable version from head ref
|
||||
id: version
|
||||
env:
|
||||
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
run: |
|
||||
version="${HEAD_REF#release/v}"
|
||||
if ! echo "$version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "::notice::Head ref '$HEAD_REF' is not a stable release — skipping downstream bump."
|
||||
exit 0
|
||||
fi
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Dispatch to internal-actions
|
||||
if: steps.version.outputs.version != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.SYNC_DISPATCH_TOKEN }}
|
||||
TARGET_REPO: ${{ vars.TARGET_REPO }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
gh api -X POST "repos/${TARGET_REPO}/dispatches" \
|
||||
-f event_type="livekit-agents-published" \
|
||||
-f client_payload[version]="$VERSION" \
|
||||
-f client_payload[source_repo]="${{ github.repository }}" \
|
||||
-f client_payload[source_sha]="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Release gate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
pull_request_review:
|
||||
types: [submitted, dismissed]
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
release-gate:
|
||||
name: Release gate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check release PR requirements
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
if [[ "$HEAD_REF" != release/v* ]]; then
|
||||
echo "Not a release PR, skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$PR_AUTHOR" != "github-actions[bot]" ]; then
|
||||
echo "::error::Release PRs must be created by the publish workflow, not by '$PR_AUTHOR'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
approvals=$(gh api "repos/$REPO/pulls/$PR_NUMBER/reviews" \
|
||||
--jq '[group_by(.user.login)[] | sort_by(.submitted_at) | last | select(.state == "APPROVED") | .user.login] | length')
|
||||
echo "Approvals: $approvals"
|
||||
if [ "$approvals" -lt 2 ]; then
|
||||
echo "::error::Release PRs require at least 2 approvals (got $approvals)"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,55 @@
|
||||
name: test-realtime
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "Branch or revision to test"
|
||||
required: false
|
||||
type: string
|
||||
default: "main"
|
||||
pull_request:
|
||||
paths:
|
||||
- "livekit-plugins/livekit-plugins-openai/livekit/plugins/openai/realtime/**"
|
||||
- "livekit-plugins/livekit-plugins-xai/livekit/plugins/xai/realtime/**"
|
||||
- "tests/test_realtime/**"
|
||||
- ".github/workflows/test-realtime.yml"
|
||||
|
||||
jobs:
|
||||
test-realtime:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.branch != '' && github.event.inputs.branch || github.ref }}
|
||||
lfs: true
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- name: Run Realtime tests
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
|
||||
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
|
||||
AZURE_OPENAI_DEPLOYMENT: ${{ secrets.AZURE_OPENAI_DEPLOYMENT }}
|
||||
AZURE_OPENAI_API_VERSION: ${{ secrets.AZURE_OPENAI_API_VERSION }}
|
||||
# XAI_API_KEY not set — xAI rate-limits GitHub Actions IPs (429 on ws handshake)
|
||||
run: |
|
||||
uv run pytest --realtime -v -s --tb=long -p no:xdist
|
||||
@@ -0,0 +1,271 @@
|
||||
name: Test STT
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "Branch or revision to test"
|
||||
required: false
|
||||
type: string
|
||||
default: "main"
|
||||
pr_number:
|
||||
description: "PR number to post results to (optional)"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
pull_request:
|
||||
paths:
|
||||
- "tests/test_stt.py"
|
||||
- ".github/workflows/test-stt.yml"
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
slash-command-dispatch:
|
||||
if: github.event_name == 'issue_comment' && github.event.issue.pull_request
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
actions: write
|
||||
steps:
|
||||
- name: Get PR details and check authorization
|
||||
id: pr-info
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
const comment = context.payload.comment.body.trim();
|
||||
const isCommand = /^\/test-stt(\s|$)/.test(comment);
|
||||
|
||||
if (!isCommand) {
|
||||
core.setOutput('is_command', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get PR details
|
||||
const prResponse = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.issue.number,
|
||||
});
|
||||
|
||||
const pr = prResponse.data;
|
||||
const commenter = context.payload.comment.user.login;
|
||||
|
||||
// Check if commenter is an organization member (not just a collaborator)
|
||||
let isAuthorized = false;
|
||||
|
||||
try {
|
||||
// First check if the repo owner is an organization
|
||||
const orgResponse = await github.rest.orgs.get({
|
||||
org: context.repo.owner,
|
||||
});
|
||||
|
||||
// If it's an organization, check if user is a member
|
||||
if (orgResponse.data) {
|
||||
try {
|
||||
await github.rest.orgs.checkMembershipForUser({
|
||||
org: context.repo.owner,
|
||||
username: commenter,
|
||||
});
|
||||
isAuthorized = true;
|
||||
console.log(`${commenter} is an organization member`);
|
||||
} catch (memberError) {
|
||||
// User is not an organization member
|
||||
isAuthorized = false;
|
||||
console.log(`${commenter} is not an organization member`);
|
||||
}
|
||||
}
|
||||
} catch (orgError) {
|
||||
// Repo owner is not an organization (it's a user account)
|
||||
// In this case, check if commenter is the repo owner
|
||||
if (commenter === context.repo.owner) {
|
||||
isAuthorized = true;
|
||||
console.log(`${commenter} is the repository owner`);
|
||||
} else {
|
||||
isAuthorized = false;
|
||||
console.log(`${commenter} is not authorized (repo is user-owned, not org-owned)`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAuthorized) {
|
||||
console.log(`Slash command rejected: ${commenter} is not an organization member`);
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: context.payload.comment.id,
|
||||
content: '-1'
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.issue.number,
|
||||
body: '❌ `/test-stt` command is only available for organization members.'
|
||||
});
|
||||
core.setOutput('is_command', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('is_command', 'true');
|
||||
core.setOutput('pr_number', pr.number.toString());
|
||||
core.setOutput('branch', pr.head.ref);
|
||||
|
||||
console.log(`Slash command /test-stt detected for PR #${pr.number} by ${commenter}`);
|
||||
|
||||
- name: Trigger workflow
|
||||
if: steps.pr-info.outputs.is_command == 'true'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'test-stt.yml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
pr_number: '${{ steps.pr-info.outputs.pr_number }}',
|
||||
branch: 'refs/pull/${{ steps.pr-info.outputs.pr_number }}/head'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Workflow triggered successfully');
|
||||
|
||||
// Add reaction to comment
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: context.payload.comment.id,
|
||||
content: 'rocket'
|
||||
});
|
||||
|
||||
test-stt:
|
||||
if: github.event_name != 'issue_comment'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.branch != '' && github.event.inputs.branch || github.ref }}
|
||||
fetch-depth: 0
|
||||
lfs: true
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv sync --all-extras --dev
|
||||
|
||||
- name: Setup Google credentials
|
||||
shell: bash
|
||||
run: |
|
||||
printf '%s' '${{ secrets.GOOGLE_STT_CREDENTIALS_JSON }}' > ${{ github.workspace }}/tests/google.json
|
||||
|
||||
- name: Run STT tests
|
||||
env:
|
||||
LIVEKIT_API_KEY: ${{ secrets.LIVEKIT_API_KEY }}
|
||||
LIVEKIT_API_SECRET: ${{ secrets.LIVEKIT_API_SECRET }}
|
||||
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
|
||||
ASSEMBLYAI_API_KEY: ${{ secrets.ASSEMBLYAI_API_KEY }}
|
||||
SPEECHMATICS_API_KEY: ${{ secrets.SPEECHMATICS_API_KEY }}
|
||||
ELEVEN_API_KEY: ${{ secrets.ELEVEN_API_KEY }}
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKSAI_API_KEY }}
|
||||
GLADIA_API_KEY: ${{ secrets.GLADIA_API_KEY }}
|
||||
FAL_KEY: ${{ secrets.FAL_KEY }}
|
||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||
NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
CARTESIA_API_KEY: ${{ secrets.CARTESIA_API_KEY }}
|
||||
GRADIUM_API_KEY: ${{ secrets.GRADIUM_API_KEY }}
|
||||
SONIOX_API_KEY: ${{ secrets.SONIOX_API_KEY }}
|
||||
GOOGLE_APPLICATION_CREDENTIALS: ${{ github.workspace }}/tests/google.json
|
||||
AZURE_SPEECH_KEY: ${{ secrets.AZURE_SPEECH_KEY }}
|
||||
AZURE_SPEECH_REGION: ${{ secrets.AZURE_SPEECH_REGION }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
# AWS_REGION: ${{ secrets.AWS_REGION }}
|
||||
SARVAM_API_KEY: ${{ secrets.SARVAM_API_KEY }}
|
||||
run: |
|
||||
uv run pytest -n 3 -v --stt --tb=long --junitxml=test-results.xml 2>&1 | tee test-output.txt || true
|
||||
|
||||
- name: Cleanup Google credentials
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
rm -f ${{ github.workspace }}/tests/google.json
|
||||
|
||||
- name: Generate test summary
|
||||
if: always()
|
||||
id: test-summary
|
||||
run: |
|
||||
python3 scripts/generate_test_summary.py test-results.xml -o test-summary.md
|
||||
|
||||
- name: Post results to PR
|
||||
if: always() && (github.event_name == 'pull_request' || github.event.inputs.pr_number != '')
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const summary = fs.readFileSync('test-summary.md', 'utf8');
|
||||
|
||||
let prNumber;
|
||||
if ('${{ github.event_name }}' === 'pull_request') {
|
||||
prNumber = ${{ github.event.pull_request.number || 0 }};
|
||||
} else if ('${{ github.event.inputs.pr_number }}') {
|
||||
prNumber = parseInt('${{ github.event.inputs.pr_number }}');
|
||||
} else {
|
||||
prNumber = null;
|
||||
}
|
||||
|
||||
if (!prNumber || isNaN(prNumber)) {
|
||||
console.log('No valid PR number, skipping comment');
|
||||
return;
|
||||
}
|
||||
|
||||
const body = `${summary}\n\n---\n*Triggered by workflow run [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})*`;
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
|
||||
const botComment = comments.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body.includes('## STT Test Results')
|
||||
);
|
||||
|
||||
if (botComment) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
body: body
|
||||
});
|
||||
console.log('Updated existing comment');
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body: body
|
||||
});
|
||||
console.log('Created new comment');
|
||||
}
|
||||
|
||||
- name: Add to job summary
|
||||
if: always()
|
||||
run: |
|
||||
cat test-summary.md >> $GITHUB_STEP_SUMMARY
|
||||
@@ -0,0 +1,199 @@
|
||||
name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
blockguard-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Build and install blockguard
|
||||
run: pip install livekit-plugins/livekit-blockguard/
|
||||
|
||||
- name: Install pytest
|
||||
run: pip install pytest
|
||||
|
||||
- name: Run blockguard tests
|
||||
working-directory: livekit-plugins/livekit-blockguard
|
||||
run: python -m pytest test_blockguard.py -v
|
||||
|
||||
unit-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install the project
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- name: Download NLTK data
|
||||
run: uv run python -c "import nltk; nltk.download('punkt_tab')"
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
run: make unit-tests
|
||||
|
||||
evaluation:
|
||||
# don't run tests for PRs on forks
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install the project
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- name: Run evals
|
||||
env:
|
||||
LIVEKIT_API_KEY: ${{ secrets.LIVEKIT_API_KEY }}
|
||||
LIVEKIT_API_SECRET: ${{ secrets.LIVEKIT_API_SECRET }}
|
||||
|
||||
run: uv run pytest --evals
|
||||
|
||||
tests:
|
||||
# don't run tests for PRs on forks
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
plugin:
|
||||
[
|
||||
cartesia,
|
||||
deepgram,
|
||||
elevenlabs,
|
||||
groq,
|
||||
openai,
|
||||
inworld,
|
||||
]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
name: livekit-plugins-${{ matrix.plugin }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
|
||||
- name: Setup Google credentials
|
||||
if: matrix.plugin == 'google'
|
||||
shell: bash
|
||||
run: |
|
||||
printf '%s' '${{ secrets.GOOGLE_CREDENTIALS_JSON }}' > tests/google.json
|
||||
|
||||
- name: Run tests
|
||||
shell: bash
|
||||
env:
|
||||
PLUGIN: ${{ matrix.plugin }}
|
||||
LIVEKIT_URL: ${{ secrets.LIVEKIT_URL }}
|
||||
LIVEKIT_API_KEY: ${{ secrets.LIVEKIT_API_KEY }}
|
||||
LIVEKIT_API_SECRET: ${{ secrets.LIVEKIT_API_SECRET }}
|
||||
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
||||
ELEVEN_API_KEY: ${{ secrets.ELEVEN_API_KEY }}
|
||||
CARTESIA_API_KEY: ${{ secrets.CARTESIA_API_KEY }}
|
||||
AZURE_SPEECH_KEY: ${{ secrets.AZURE_SPEECH_KEY }}
|
||||
AZURE_SPEECH_REGION: ${{ secrets.AZURE_SPEECH_REGION }} # nit: doesn't have to be secret
|
||||
GOOGLE_CREDENTIALS_JSON: ${{ secrets.GOOGLE_CREDENTIALS_JSON }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
ASSEMBLYAI_API_KEY: ${{ secrets.ASSEMBLYAI_API_KEY }}
|
||||
FAL_KEY: ${{ secrets.FAL_KEY }}
|
||||
PLAYHT_API_KEY: ${{ secrets.PLAYHT_API_KEY }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
PLAYHT_USER_ID: ${{ secrets.PLAYHT_USER_ID }}
|
||||
RIME_API_KEY: ${{ secrets.RIME_API_KEY }}
|
||||
SPEECHMATICS_API_KEY: ${{ secrets.SPEECHMATICS_API_KEY }}
|
||||
GOOGLE_APPLICATION_CREDENTIALS: tests/google.json
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
NEUPHONIC_API_KEY: ${{ secrets.NEUPHONIC_API_KEY }}
|
||||
RESEMBLE_API_KEY: ${{ secrets.RESEMBLE_API_KEY }}
|
||||
SPEECHIFY_API_KEY: ${{ secrets.SPEECHIFY_API_KEY }}
|
||||
HUME_API_KEY: ${{ secrets.HUME_API_KEY }}
|
||||
SPITCH_API_KEY: ${{ secrets.SPITCH_API_KEY }}
|
||||
LMNT_API_KEY: ${{ secrets.LMNT_API_KEY }}
|
||||
INWORLD_API_KEY: ${{ secrets.INWORLD_API_KEY }}
|
||||
SONIOX_API_KEY: ${{ secrets.SONIOX_API_KEY }}
|
||||
working-directory: tests
|
||||
run: make test
|
||||
|
||||
- name: Upload LiveKit dump
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: lk-dump-${{ matrix.plugin }}
|
||||
path: lk_dump/**
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
|
||||
aggregate-dumps:
|
||||
if: always()
|
||||
needs: tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: download all dump shards
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
pattern: lk-dump-*
|
||||
path: all_lk_dump
|
||||
merge-multiple: true
|
||||
|
||||
- name: create tarball
|
||||
id: tar
|
||||
run: |
|
||||
if [ -d "all_lk_dump" ] && [ "$(ls -A all_lk_dump)" ]; then
|
||||
tar -czf lk-dump-all.tar.gz -C all_lk_dump .
|
||||
echo "created=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "No dump shards found, skipping tarball creation."
|
||||
echo "created=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: upload merged dump
|
||||
if: steps.tar.outputs.created == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: lk-dump-all
|
||||
path: lk-dump-all.tar.gz
|
||||
retention-days: 5
|
||||
Reference in New Issue
Block a user