SauceLabs CI/CD Integration: GitHub Actions, Jenkins, and CircleCI
SauceLabs fits into any CI/CD pipeline as a remote WebDriver endpoint. The integration pattern is the same across GitHub Actions, Jenkins, and CircleCI: store credentials as secrets, optionally start a Sauce Connect tunnel for internal URLs, run your test suite pointing at SauceLabs, then collect results. This post shows concrete configs for each CI platform.
The Integration Pattern
Every SauceLabs CI integration follows this structure:
- Store
SAUCE_USERNAMEandSAUCE_ACCESS_KEYas secrets/environment variables - Optionally start Sauce Connect Tunnel (for internal URLs)
- Run tests pointing at SauceLabs endpoint
- Collect test results and artifacts (video, screenshots)
- Stop the tunnel
The SauceLabs endpoint is always:
https://SAUCE_USERNAME:SAUCE_ACCESS_KEY@ondemand.us-west-1.saucelabs.com/wd/hubGitHub Actions
Basic Setup (No Tunnel)
# .github/workflows/cross-browser.yml
name: Cross-Browser Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chrome, firefox, safari]
fail-fast: false # Don't cancel other browsers if one fails
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run ${{ matrix.browser }} tests
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
BROWSER: ${{ matrix.browser }}
BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
BUILD_NAME: "PR-${{ github.event.pull_request.number || 'main' }}"
run: |
pytest tests/e2e/ \
--browser=$BROWSER \
--tb=short \
--junit-xml=results-${{ matrix.browser }}.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.browser }}
path: results-*.xmlWith Sauce Connect Tunnel
name: Integration Tests with Sauce Connect
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Start Sauce Connect Tunnel
uses: saucelabs/sauce-connect-action@v2
with:
username: ${{ secrets.SAUCE_USERNAME }}
accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
tunnelName: "github-${{ github.run_id }}"
# Optional: configure which domains go through tunnel
# tunnelDomains: "localhost,staging.internal.com"
- name: Start local server
run: |
npm run build
npm run start &
# Wait for server to be ready
timeout 30 bash -c 'until curl -sf http://localhost:3000/; do sleep 1; done'
- name: Run tests
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
SAUCE_TUNNEL_NAME: "github-${{ github.run_id }}"
APP_URL: "http://localhost:3000"
BUILD_ID: ${{ github.run_id }}
run: pytest tests/ -n 4 --tb=shortParallel Jobs with Matrix
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- browser: chrome
platform: "Windows 11"
browser-version: latest
- browser: firefox
platform: "Windows 11"
browser-version: latest
- browser: safari
platform: "macOS 14"
browser-version: latest
- browser: MicrosoftEdge
platform: "Windows 11"
browser-version: latest
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
- name: Run tests on ${{ matrix.browser }}
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
BROWSER: ${{ matrix.browser }}
BROWSER_VERSION: ${{ matrix.browser-version }}
PLATFORM: ${{ matrix.platform }}
BUILD_ID: ${{ github.run_id }}
run: pytest tests/ --tb=short --junit-xml=results.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: results-${{ matrix.browser }}
path: results.xmlJenkins
Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
environment {
SAUCE_USERNAME = credentials('sauce-username')
SAUCE_ACCESS_KEY = credentials('sauce-access-key')
BUILD_ID = "${env.BUILD_NUMBER}"
}
stages {
stage('Install') {
steps {
sh 'pip install -r requirements.txt'
}
}
stage('Start Sauce Connect') {
steps {
script {
// Download Sauce Connect if not present
sh '''
if [ ! -f ./sc ]; then
curl -L https://saucelabs.com/downloads/sauce-connect/5.1.3/sauce-connect-5.1.3_linux.x86_64.tar.gz | tar xz
fi
'''
// Start tunnel in background
sh "./sc run --username $SAUCE_USERNAME --access-key $SAUCE_ACCESS_KEY --tunnel-name jenkins-${BUILD_NUMBER} --daemon"
// Wait for tunnel ready
sh "timeout 60 bash -c 'until ./sc status --tunnel-name jenkins-${BUILD_NUMBER}; do sleep 2; done'"
}
}
}
stage('Test') {
parallel {
stage('Chrome') {
environment { BROWSER = 'chrome' }
steps {
sh "pytest tests/ --browser=chrome --junit-xml=results-chrome.xml"
}
post {
always {
junit 'results-chrome.xml'
}
}
}
stage('Firefox') {
environment { BROWSER = 'firefox' }
steps {
sh "pytest tests/ --browser=firefox --junit-xml=results-firefox.xml"
}
post {
always {
junit 'results-firefox.xml'
}
}
}
}
}
}
post {
always {
sh "./sc stop --tunnel-name jenkins-${BUILD_NUMBER} || true"
}
failure {
// Collect SauceLabs job IDs from test output
sh '''
JOB_IDS=$(grep -o 'SauceJobID=[a-z0-9-]*' test.log | cut -d= -f2)
for id in $JOB_IDS; do
curl -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \
"https://api.us-west-1.saucelabs.com/rest/v1/$SAUCE_USERNAME/jobs/$id/assets/video.mp4" \
--output "video-$id.mp4"
done
'''
archiveArtifacts artifacts: 'video-*.mp4', allowEmptyArchive: true
}
}
}CircleCI
# .circleci/config.yml
version: 2.1
orbs:
browser-tools: circleci/browser-tools@1.4
executors:
python:
docker:
- image: cimg/python:3.12
environment:
SAUCE_USERNAME: $SAUCE_USERNAME
SAUCE_ACCESS_KEY: $SAUCE_ACCESS_KEY
commands:
setup-sauce-connect:
steps:
- run:
name: Install Sauce Connect
command: |
curl -L https://saucelabs.com/downloads/sauce-connect/5.1.3/sauce-connect-5.1.3_linux.x86_64.tar.gz | tar xz -C /usr/local/bin/
- run:
name: Start Sauce Connect
command: sc run --username $SAUCE_USERNAME --access-key $SAUCE_ACCESS_KEY --tunnel-name "circleci-$CIRCLE_BUILD_NUM" --daemon
background: true
- run:
name: Wait for tunnel
command: timeout 60 bash -c 'until sc status --tunnel-name circleci-$CIRCLE_BUILD_NUM 2>/dev/null; do sleep 2; done'
jobs:
cross-browser-test:
executor: python
parallelism: 4 # 4 parallel containers
steps:
- checkout
- restore_cache:
keys:
- deps-{{ checksum "requirements.txt" }}
- run: pip install -r requirements.txt
- save_cache:
key: deps-{{ checksum "requirements.txt" }}
paths: [~/.cache/pip]
- setup-sauce-connect
- run:
name: Run tests (split across containers)
environment:
BUILD_ID: $CIRCLE_BUILD_NUM
SAUCE_TUNNEL_NAME: circleci-$CIRCLE_BUILD_NUM
command: |
# CircleCI test splitting distributes tests across containers
TEST_FILES=$(circleci tests glob "tests/**/*.py" | circleci tests split --split-by=timings)
pytest $TEST_FILES --junit-xml=test-results/results.xml
- store_test_results:
path: test-results
- store_artifacts:
path: test-results
destination: test-results
workflows:
cross-browser:
jobs:
- cross-browser-testTest Reporting
Linking SauceLabs Jobs to CI Builds
Pass the CI build identifier into SauceLabs metadata:
import os
def get_sauce_options(test_name: str) -> dict:
return {
"username": os.environ["SAUCE_USERNAME"],
"accessKey": os.environ["SAUCE_ACCESS_KEY"],
"name": test_name,
"build": os.environ.get("BUILD_ID", "local"),
# CI-specific metadata
"customData": {
"ci": os.environ.get("CI", "false"),
"branch": os.environ.get("GITHUB_REF_NAME", "local"),
"commit": os.environ.get("GITHUB_SHA", "unknown"),
"pr": os.environ.get("GITHUB_EVENT_NUMBER", ""),
}
}Downloading Failure Artifacts
When a test fails, retrieve video and screenshots:
# Get failed job IDs from last build
FAILED_JOBS=$(curl -s -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \
"https://api.us-west-1.saucelabs.com/rest/v1/$SAUCE_USERNAME/jobs?limit=50&full=true" \
| jq -r '.[] | select(.passed == false) | .id')
for JOB_ID in $FAILED_JOBS; do
# Download video
curl -s -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \
"https://api.us-west-1.saucelabs.com/rest/v1/$SAUCE_USERNAME/jobs/$JOB_ID/assets/video.mp4" \
--output "artifacts/video-${JOB_ID}.mp4"
# Download final screenshot
curl -s -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \
"https://api.us-west-1.saucelabs.com/rest/v1/$SAUCE_USERNAME/jobs/$JOB_ID/assets/final_screenshot.png" \
--output "artifacts/screenshot-${JOB_ID}.png"
doneConcurrency Management
Avoid hitting concurrency limits by controlling the number of parallel sessions:
# GitHub Actions: limit matrix parallelism
jobs:
test:
strategy:
matrix:
browser: [chrome, firefox, safari, edge]
max-parallel: 3 # Don't exceed SauceLabs concurrency limitFor pytest-xdist, pass the concurrency limit:
pytest tests/ -n 5 # Match your SauceLabs plan limitSauce Connect in Docker
If your CI runs in Docker:
FROM python:3.12-slim
RUN curl -L \
https://saucelabs.com/downloads/sauce-connect/5.1.3/sauce-connect-5.1.3_linux.x86_64.tar.gz \
| tar xz -C /usr/local/bin/ --strip-components=1
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["sh", "-c", \
"sc run --username $SAUCE_USERNAME --access-key $SAUCE_ACCESS_KEY --tunnel-name docker-$BUILD_ID --daemon && \
pytest tests/ && \
sc stop --tunnel-name docker-$BUILD_ID"]