> elevenlabs-ci-integration

Configure CI/CD pipelines for ElevenLabs with mocked unit tests and gated integration tests. Use when setting up GitHub Actions for TTS projects, configuring CI test strategies, or automating ElevenLabs integration validation. Trigger: "elevenlabs CI", "elevenlabs GitHub Actions", "elevenlabs automated tests", "CI elevenlabs", "elevenlabs pipeline".

fetch
$curl "https://skillshub.wtf/jeremylongshore/claude-code-plugins-plus-skills/elevenlabs-ci-integration?format=md"
SKILL.mdelevenlabs-ci-integration

ElevenLabs CI Integration

Overview

Set up CI/CD pipelines that test ElevenLabs integrations without burning character quota on every PR. Uses a two-tier strategy: mocked unit tests on every push, gated integration tests on demand.

Prerequisites

  • GitHub repository with Actions enabled
  • ElevenLabs API key for integration tests
  • npm/pnpm project with vitest configured

Instructions

Step 1: GitHub Actions Workflow

# .github/workflows/elevenlabs-tests.yml
name: ElevenLabs Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  # Tier 1: Always runs — no API key needed, no quota cost
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"
      - run: npm ci
      - run: npm test -- --coverage
        env:
          # Mock mode — no real API calls
          ELEVENLABS_API_KEY: "sk_test_mock_key_for_ci"

  # Tier 2: Only on main or manual trigger — uses real API
  integration-tests:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
    needs: unit-tests
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"
      - run: npm ci

      # Check quota before running integration tests
      - name: Check ElevenLabs quota
        env:
          ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }}
        run: |
          REMAINING=$(curl -s https://api.elevenlabs.io/v1/user \
            -H "xi-api-key: ${ELEVENLABS_API_KEY}" | \
            jq '.subscription | (.character_limit - .character_count)')
          echo "Characters remaining: $REMAINING"
          if [ "$REMAINING" -lt 5000 ]; then
            echo "::warning::Low ElevenLabs quota ($REMAINING chars). Skipping integration tests."
            echo "SKIP_INTEGRATION=true" >> $GITHUB_ENV
          fi

      - name: Run integration tests
        if: env.SKIP_INTEGRATION != 'true'
        env:
          ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }}
          ELEVENLABS_INTEGRATION: "1"
        run: npm run test:integration

Step 2: Configure Repository Secrets

# Store API key as GitHub secret (use a test/dev key, NOT production)
gh secret set ELEVENLABS_API_KEY --body "sk_your_test_key_here"

# Optional: webhook secret for webhook tests
gh secret set ELEVENLABS_WEBHOOK_SECRET --body "whsec_your_secret_here"

Step 3: Unit Test with SDK Mock

// tests/unit/tts-service.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";

// Mock the entire SDK — no API calls, no quota usage
vi.mock("@elevenlabs/elevenlabs-js", () => ({
  ElevenLabsClient: vi.fn().mockImplementation(() => ({
    textToSpeech: {
      convert: vi.fn().mockResolvedValue(
        new ReadableStream({
          start(controller) {
            controller.enqueue(new Uint8Array([0xFF, 0xFB, 0x90, 0x00])); // MP3 header
            controller.close();
          },
        })
      ),
      stream: vi.fn().mockImplementation(async function* () {
        yield new Uint8Array([0xFF, 0xFB, 0x90, 0x00]);
      }),
    },
    voices: {
      getAll: vi.fn().mockResolvedValue({
        voices: [
          { voice_id: "21m00Tcm4TlvDq8ikWAM", name: "Rachel", category: "premade" },
        ],
      }),
    },
    user: {
      get: vi.fn().mockResolvedValue({
        subscription: { tier: "pro", character_count: 1000, character_limit: 500000 },
      }),
    },
  })),
}));

import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";

describe("TTS Service", () => {
  let client: InstanceType<typeof ElevenLabsClient>;

  beforeEach(() => {
    client = new ElevenLabsClient();
  });

  it("should call TTS with correct parameters", async () => {
    await client.textToSpeech.convert("21m00Tcm4TlvDq8ikWAM", {
      text: "Test speech",
      model_id: "eleven_multilingual_v2",
      voice_settings: { stability: 0.5, similarity_boost: 0.75 },
    });

    expect(client.textToSpeech.convert).toHaveBeenCalledWith(
      "21m00Tcm4TlvDq8ikWAM",
      expect.objectContaining({
        text: "Test speech",
        model_id: "eleven_multilingual_v2",
      })
    );
  });

  it("should handle voice listing", async () => {
    const result = await client.voices.getAll();
    expect(result.voices).toHaveLength(1);
    expect(result.voices[0].name).toBe("Rachel");
  });
});

Step 4: Integration Test (Gated)

// tests/integration/tts-smoke.test.ts
import { describe, it, expect } from "vitest";

const SKIP = !process.env.ELEVENLABS_INTEGRATION;

describe.skipIf(SKIP)("ElevenLabs Integration", () => {
  it("should generate audio from text", async () => {
    const { ElevenLabsClient } = await import("@elevenlabs/elevenlabs-js");
    const client = new ElevenLabsClient();

    // Use Flash model + short text to minimize quota usage
    const audio = await client.textToSpeech.convert("21m00Tcm4TlvDq8ikWAM", {
      text: "CI test.",  // 8 characters = 4 credits (Flash)
      model_id: "eleven_flash_v2_5",
      output_format: "mp3_22050_32",
    });

    expect(audio).toBeDefined();
  }, 30_000);

  it("should list voices", async () => {
    const { ElevenLabsClient } = await import("@elevenlabs/elevenlabs-js");
    const client = new ElevenLabsClient();
    const { voices } = await client.voices.getAll();
    expect(voices.length).toBeGreaterThan(0);
  });
});

Step 5: Package Scripts

{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest --watch",
    "test:integration": "ELEVENLABS_INTEGRATION=1 vitest run tests/integration/",
    "test:ci": "vitest run --coverage --reporter=junit --outputFile=test-results.xml"
  }
}

CI Strategy Summary

TierWhenAPI KeyQuota CostCoverage
Unit testsEvery push/PRMock key0 charactersSDK integration patterns
IntegrationMain + manualReal test key~50 charsEnd-to-end TTS verification
Quota checkBefore integrationReal test key0 (GET only)Prevents surprise billing

Error Handling

IssueCauseSolution
Secret not found in CIMissing repository secretgh secret set ELEVENLABS_API_KEY
Integration tests timeoutSlow TTS generationIncrease test timeout to 30s; use Flash model
Quota depleted in CIToo many integration runsUse quota guard; limit to main branch only
Mock driftSDK API changedUpdate mocks when upgrading SDK

Resources

Next Steps

For deployment patterns, see elevenlabs-deploy-integration.

┌ stats

installs/wk0
░░░░░░░░░░
github stars1.7K
██████████
first seenMar 23, 2026
└────────────

┌ repo

jeremylongshore/claude-code-plugins-plus-skills
by jeremylongshore
└────────────