DevOps & Skalierung

GitHub Actions + CaptchaAI: CI/CD CAPTCHA-Test

Ihr CI-Job muss kein CAPTCHA anklicken. Sie hinterlegen den CaptchaAI-API-Schlüssel als Repository-Secret, der Test-Runner fordert das Token über die API an und trägt es in das Formularfeld ein – der Rest der Pipeline läuft unverändert weiter.

Das Szenario ist in fast jedem Team dasselbe: Der nächtliche Lauf der Integrationstests ist rot, das Log endet beim Login-Formular, und lokal funktioniert alles. Zwischen Runner und Anwendung steht eine reCAPTCHA-v2-Abfrage, die kein Headless-Browser von allein beantwortet. Die folgenden sechs Schritte führen von der Secret-Ablage bis zur Slack-Meldung bei rotem Lauf.


Voraussetzungen

  • Ein CaptchaAI-Konto mit aktivem Plan – abgerechnet wird pro Thread, nicht pro Lösung.
  • Ein Repository, in dem GitHub Actions aktiviert ist.
  • Python 3.11 im Runner sowie die Pakete requests und pytest.
  • Eine Testumgebung, die Sie selbst betreiben: Staging, Vorschau-Deployment oder eine lokal gehostete Instanz. Fremde Produktivseiten gehören nicht in eine Testsuite.

Schritt 1: API-Schlüssel als Repository-Secret hinterlegen

  1. Öffnen Sie im Repository Settings → Secrets and variables → Actions.
  2. Klicken Sie auf New repository secret.
  3. Name: CAPTCHAAI_KEY
  4. Wert: Ihr CaptchaAI-API-Schlüssel, ohne führende oder abschließende Leerzeichen.
  5. Speichern über Add secret.

Der Schlüssel gehört nie in die Workflow-YAML, in eine .env im Repository oder in eine Fixture-Datei. Zwei Details sparen später Zeit: Ein Environment-Secret lässt sich zusätzlich auf den main-Branch beschränken, und Secrets stehen Workflow-Läufen aus fremden Forks grundsätzlich nicht zur Verfügung. Der Testcode aus Schritt 3 fängt genau diesen Fall ab – ohne Schlüssel wird der Solve-Test übersprungen statt rot.


Schritt 2: Workflow-Datei für die CAPTCHA-Tests

# .github/workflows/captcha-tests.yml
name: CAPTCHA Integration Tests

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

    - cron: "0 6 * * 1"  # Weekly Monday 6 AM

jobs:
  captcha-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:

      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: pip install requests pytest

      - name: Run CAPTCHA integration tests
        env:
          CAPTCHAAI_KEY: ${{ secrets.CAPTCHAAI_KEY }}
        run: pytest tests/test_captcha.py -v --tb=short

Drei Auslöser decken den Alltag ab: jeder Push auf main, jeder Pull Request und ein wöchentlicher Cron-Lauf am Montagmorgen. timeout-minutes: 15 ist die Notbremse, falls ein Job hängen bleibt – der Wert liegt bewusst deutlich über der Lösungszeit einer einzelnen Abfrage. Der Schlüssel wird ausschließlich als env-Variable in den Testschritt gereicht, nicht global im Workflow gesetzt.


Schritt 3: Testdatei mit pytest

# tests/test_captcha.py
import os
import time
import pytest
import requests


API_KEY = os.environ.get("CAPTCHAAI_KEY")
BASE_URL = "https://ocr.captchaai.com"


def solve_recaptcha(site_key, page_url, timeout=90):
    """Solve reCAPTCHA v2 via CaptchaAI."""
    resp = requests.post(f"{BASE_URL}/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": site_key,
        "pageurl": page_url,
        "json": 1,
    }, timeout=30)
    result = resp.json()
    assert result.get("status") == 1, f"Submit failed: {result}"

    task_id = result["request"]
    start = time.time()

    while time.time() - start < timeout:
        time.sleep(5)
        resp = requests.get(f"{BASE_URL}/res.php", params={
            "key": API_KEY,
            "action": "get",
            "id": task_id,
            "json": 1,
        }, timeout=15)
        data = resp.json()
        if data["request"] != "CAPCHA_NOT_READY":
            assert data.get("status") == 1, f"Solve failed: {data}"
            return data["request"]

    pytest.fail("CAPTCHA solve timed out")


@pytest.mark.skipif(not API_KEY, reason="CAPTCHAAI_KEY not set")
class TestCaptchaIntegration:
    """Integration tests for CAPTCHA-protected flows."""

    def test_recaptcha_v2_solve(self):
        """Verify CaptchaAI can solve reCAPTCHA v2."""
        token = solve_recaptcha(
            site_key="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
            page_url="https://www.google.com/recaptcha/api2/demo",
        )
        assert len(token) > 100
        assert token.isascii()

    def test_balance_sufficient(self):
        """Ensure account balance is enough for test suite."""
        resp = requests.get(f"{BASE_URL}/res.php", params={
            "key": API_KEY,
            "action": "getbalance",
            "json": 1,
        })
        balance = float(resp.json()["request"])
        assert balance > 0.50, f"Low balance: ${balance}"

    def test_api_key_valid(self):
        """Verify API key is accepted."""
        resp = requests.get(f"{BASE_URL}/res.php", params={
            "key": API_KEY,
            "action": "getbalance",
            "json": 1,
        })
        result = resp.json()
        assert result.get("status") == 1, f"Invalid key: {result}"

Der Ablauf folgt dem Zwei-Schritt-Muster der API: in.php nimmt googlekey und pageurl entgegen und liefert eine Task-ID zurück; res.php wird anschließend im Fünf-Sekunden-Takt abgefragt, bis der Status nicht mehr CAPCHA_NOT_READY lautet. Erst dann steht das Token bereit und kann in das Formularfeld eingetragen werden.

Die beiden kleinen Zusatztests laufen in Sekunden und beantworten bei einem roten Lauf sofort die entscheidende Frage: Konfiguration oder getestete Anwendung?


Timeouts an realistische Lösungszeiten koppeln

Ein Test-Timeout, das knapper bemessen ist als die Lösungszeit des jeweiligen Typs, erzeugt Fehlschläge, die niemand reproduzieren kann. Als Faustregel gilt: mindestens das Doppelte der Obergrenze, bei schnellen Typen ein Vielfaches – dort dominieren Netzlatenz und Wartezeit im Runner.

CAPTCHA-Typ Lösungszeit (Obergrenze) Praxiswert im Test
Bild-CAPTCHA < 0,5 s 30 s
Cloudflare Turnstile < 10 s 60 s
GeeTest v3 < 12 s 60 s
reCAPTCHA v2 < 60 s 120 s

Die linke Spalte nennt Obergrenzen, keine Mittelwerte; für die Pipeline zählt trotzdem der ungünstige Fall.


Schritt 4: Matrix-Läufe über mehrere CAPTCHA-Typen

jobs:
  captcha-matrix:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        captcha-type: [recaptcha-v2, turnstile, image]
      fail-fast: false

    steps:

      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: pip install requests pytest

      - name: Run ${{ matrix.captcha-type }} tests
        env:
          CAPTCHAAI_KEY: ${{ secrets.CAPTCHAAI_KEY }}
          CAPTCHA_TYPE: ${{ matrix.captcha-type }}
        run: pytest tests/test_${{ matrix.captcha-type }}.py -v

fail-fast: false sorgt dafür, dass ein fehlgeschlagener Typ die übrigen Läufe nicht abbricht: Sie sehen in einem Durchgang, ob nur ein Typ betroffen ist oder die gesamte Integration klemmt.

Nehmen Sie in die Matrix nur Typen auf, die CaptchaAI tatsächlich löst. hCaptcha und FunCaptcha gehören nicht dazu, GeeTest v4 ist bislang nur als „bald verfügbar“ angekündigt. CaptchaFox (Beta), Friendly Captcha (Beta) und Lemin (Beta) lassen sich testen, gehören aber in einen separaten, nicht blockierenden Job – Beta-Typen sollten keine Release-Pipeline stoppen.


Schritt 5: Ergebnisse zwischenspeichern

Jeder Solve kostet Zeit und belegt einen Thread. Hat sich unter tests/ nichts geändert, muss die Suite auch nicht erneut lösen:


      - name: Cache test results
        uses: actions/cache@v4
        with:
          path: .test-cache
          key: captcha-tests-${{ hashFiles('tests/**') }}

      - name: Skip if cached
        id: check-cache
        run: |
          if [ -f .test-cache/passed ]; then
            echo "skip=true" >> $GITHUB_OUTPUT
          fi

      - name: Run tests
        if: steps.check-cache.outputs.skip != 'true'
        env:
          CAPTCHAAI_KEY: ${{ secrets.CAPTCHAAI_KEY }}
        run: |
          pytest tests/test_captcha.py -v
          mkdir -p .test-cache && touch .test-cache/passed

Zur Kostenseite: CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung. Für eine übliche Testsuite reicht BASIC (15 $/Monat, 5 Threads) – die Lösungen im Abrechnungsmonat sind nicht begrenzt. Erst wenn viele Matrix-Jobs gleichzeitig lösen, wird die Thread-Zahl zum Engpass – dann ist ADVANCE (90 $/Monat, 50 Threads) die passende Stufe. Alle Preise in US-Dollar.


Schritt 6: Benachrichtigung bei Fehlschlag


      - name: Notify on failure
        if: failure()
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "CAPTCHA tests failed on ${{ github.ref }}",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "CAPTCHA tests *failed* on `${{ github.ref }}`\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View run>"
                  }
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Ohne Benachrichtigung fällt ein roter Cron-Lauf oft erst beim nächsten Release auf. Achten Sie zugleich darauf, dass weder Token noch Schlüssel im Log landen – --tb=short kürzt Tracebacks entsprechend.


Fehlerbehebung

Symptom Ursache Behebung
Tests werden übersprungen, Log zeigt „CAPTCHAAI_KEY not set“ Secret fehlt, oder der Lauf stammt aus einem Fork Secret in den Repo-Einstellungen anlegen; Fork-Läufe bewusst überspringen
Lokal grün, im Runner Zeitüberschreitung Höhere Netzlatenz des Runners Timeout im Test auf 120 Sekunden anheben
Die Kontostandabfrage schlägt fehl Leerzeichen oder Zeilenumbruch im Secret-Wert Wert neu einfügen und erneut speichern
Der Workflow startet nie Trigger oder Branch-Name passen nicht on:-Block und Branch-Namen in der YAML prüfen

DACH-Praxis: GitLab CI, eigene Runner und Testdaten

Ein Kölner Shopware-Team betreibt seine Haupt-Pipeline auf GitLab CI und spiegelt nur die Release-Tests nach GitHub Actions – ein Muster, das in deutschen Unternehmen häufig vorkommt. Der Python-Teil aus Schritt 3 bleibt dabei unverändert; ausgetauscht wird nur die Workflow-Syntax, und aus dem Repository-Secret wird eine maskierte CI/CD-Variable.

Zwei weitere Punkte aus der Praxis:

  • Eigene Runner: Gehostete GitHub-Runner wechseln bei jedem Lauf die IP-Adresse. Wer stabile Ausgangs-IPs für die Freigabe in der eigenen Staging-Firewall braucht, betreibt einen selbst gehosteten Runner – etwa auf einem Server bei Hetzner, IONOS oder netcup.
  • DSGVO: Testfixtures kommen ohne echte Kundendaten aus. IP- und E-Mail-Adressen sind personenbezogene Daten; synthetische Konten auf der eigenen Staging-Umgebung ersparen die Frage nach der Rechtsgrundlage von vornherein. Prüfen Sie außerdem, wie lange CI-Logs aufbewahrt werden und wer sie einsehen kann.

FAQ

Warum läuft der Test lokal, scheitert aber im Runner?

Fast immer am Timing, nicht am Schlüssel. Gehostete Runner haben eine andere Netzlatenz als Ihr Notebook, und eine reCAPTCHA-v2-Abfrage darf bis zu einer Minute brauchen. Setzen Sie das Timeout im Test auf 120 Sekunden, lassen Sie timeout-minutes im Job bei 15 und prüfen Sie mit dem Kontostand-Test, ob der Schlüssel überhaupt ankommt.

Funktionieren Secrets in Pull Requests aus Forks?

Nein. GitHub reicht Repository-Secrets nicht an Läufe aus fremden Forks weiter, CAPTCHAAI_KEY bleibt dort leer. Genau dafür steht pytest.mark.skipif im Testcode: Der Solve-Test wird übersprungen statt rot. Die vollständige Suite läuft dann beim Merge nach main oder im geplanten Wochenlauf.

Welche CAPTCHA-Typen lassen sich in der Pipeline testen?

Alles, was CaptchaAI regulär löst: reCAPTCHA v2 und v3 samt Invisible- und Enterprise-Varianten, Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3 sowie Bild-, Rasterbild- und BLS-CAPTCHAs. CaptchaFox, Friendly Captcha und Lemin sind als Beta verfügbar. hCaptcha, FunCaptcha und GeeTest v4 stehen nicht zur Verfügung und gehören daher auch in keinen Testfall.

Wie halte ich die Laufzeit der Pipeline kurz?

Trennen Sie schnelle von teuren Prüfungen. Schlüssel- und Kontostandtests laufen bei jedem Push in wenigen Sekunden, die eigentlichen Solve-Tests beim Merge nach main oder im Wochenlauf. Der Cache-Schritt aus Schritt 5 verhindert Wiederholungen bei unverändertem Testcode, und ausreichend Threads erlauben parallele Matrix-Jobs, ohne dass die Läufe aufeinander warten.


Verwandte Leitfäden


Pipeline grün, Tests reproduzierbar – CaptchaAI in Ihre CI/CD-Kette einbinden.

Kommentare sind für diesen Artikel deaktiviert.