CI/CD Integration
How to use OneShotMail in GitHub Actions, GitLab CI, and Jenkins for automated email testing.
Overview
OneShotMail integrates into any CI/CD pipeline. The pattern is:
- Store your API key as a CI secret.
- Set the
ONESHOT_API_KEYenvironment variable. - Run your tests normally.
- Addresses auto-delete via TTL; bulk-delete by label for immediate cleanup.
GitHub Actions
Complete workflow: .github/workflows/email-tests.yml
name: Email Tests
on:
push:
branches: [main]
pull_request:
env:
ONESHOT_API_KEY: ${{ secrets.ONESHOT_API_KEY }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run email integration tests
run: |
pytest tests/email/ -v \
--tb=short \
--timeout=120 \
-x
env:
# Use the GitHub run ID as the label for easy cleanup
CI_RUN_ID: ${{ github.run_id }}
- name: Cleanup OneShotMailMail addresses
if: always()
run: |
pip install oneshot-mail
python -c "
import oneshot
oneshot.delete_by_label('ci-${{ github.run_id }}')
"
Setting the secret
# Using the GitHub CLI
gh secret set ONESHOT_API_KEY --body "osm_live_your_key"
Multiple test jobs
If you run tests in parallel jobs, use the job name in the label to avoid collisions:
jobs:
unit-tests:
runs-on: ubuntu-latest
env:
ONESHOT_API_KEY: ${{ secrets.ONESHOT_API_KEY }}
steps:
- uses: actions/checkout@v4
- run: pytest tests/unit/ -v
env:
CI_LABEL: "unit-${{ github.run_id }}"
integration-tests:
runs-on: ubuntu-latest
env:
ONESHOT_API_KEY: ${{ secrets.ONESHOT_API_KEY }}
steps:
- uses: actions/checkout@v4
- run: pytest tests/integration/ -v
env:
CI_LABEL: "integration-${{ github.run_id }}"
cleanup:
needs: [unit-tests, integration-tests]
if: always()
runs-on: ubuntu-latest
env:
ONESHOT_API_KEY: ${{ secrets.ONESHOT_API_KEY }}
steps:
- run: |
pip install oneshot-mail
python -c "
import oneshot
oneshot.delete_by_label('unit-${{ github.run_id }}')
oneshot.delete_by_label('integration-${{ github.run_id }}')
"
GitLab CI
.gitlab-ci.yml
stages:
- test
- cleanup
variables:
ONESHOT_API_KEY: $ONESHOT_API_KEY # Set in GitLab CI/CD Variables
email-tests:
stage: test
image: python:3.12
before_script:
- pip install -r requirements.txt
script:
- pytest tests/email/ -v --timeout=120
variables:
CI_RUN_ID: $CI_PIPELINE_ID
cleanup:
stage: cleanup
image: python:3.12
when: always
before_script:
- pip install oneshot-mail
script:
- python -c "import oneshot; oneshot.delete_by_label('ci-$CI_PIPELINE_ID')"
Setting the variable
In GitLab, go to Settings > CI/CD > Variables and add:
- Key:
ONESHOT_API_KEY - Value:
osm_live_your_key - Masked: Yes
- Protected: Yes (if you only want it on protected branches)
Jenkins
Jenkinsfile
pipeline {
agent any
environment {
ONESHOT_API_KEY = credentials('oneshot-api-key')
CI_RUN_ID = "${env.BUILD_ID}"
}
stages {
stage('Install') {
steps {
sh 'pip install -r requirements.txt'
}
}
stage('Email Tests') {
steps {
sh '''
pytest tests/email/ -v \
--timeout=120 \
--tb=short
'''
}
}
}
post {
always {
sh '''
pip install oneshot-mail
python -c "import oneshot; oneshot.delete_by_label('ci-${BUILD_ID}')"
'''
}
}
}
Setting the credential
In Jenkins, go to Manage Jenkins > Manage Credentials and add a Secret text credential with ID oneshot-api-key.
Best practices for CI/CD
1. Always use labels tied to the build ID
import os
run_id = os.environ.get("CI_RUN_ID", os.environ.get("BUILD_ID", "local"))
addr = oneshot.create(label=f"ci-{run_id}")
This ensures:
- Every build creates uniquely labelled addresses.
- Cleanup can target a specific build.
- Leftover addresses from failed builds are identifiable.
2. Always clean up, even on failure
Use if: always() (GitHub Actions), when: always (GitLab), or post { always { } } (Jenkins) to ensure cleanup runs regardless of test outcomes.
3. Set timeouts appropriately
CI environments may have slower network connectivity. Set generous timeouts:
email = oneshot.wait_for_email(addr.id, timeout=60) # 60s instead of default 30
4. Use a dedicated API key for CI
Create a separate OneShotMail account for CI with an appropriate plan. This:
- Keeps CI usage separate from developer usage.
- Allows independent rate limiting.
- Makes billing attribution clear.
5. Handle rate limits
If you run many parallel tests, you may hit rate limits. The SDKs raise a RateLimitedError with a retry_after value. Build retry logic into your test infrastructure:
import time
from oneshot.exceptions import RateLimitedError
def create_with_retry(label, max_retries=3):
for attempt in range(max_retries):
try:
return oneshot.create(ttl_seconds=300, label=label)
except RateLimitedError as e:
if attempt < max_retries - 1:
time.sleep(e.retry_after or 2)
else:
raise
6. Label naming conventions
Adopt a consistent label scheme across your team:
| Context | Label format | Example |
|---|---|---|
| GitHub Actions | gha-{run_id} | gha-12345678 |
| GitLab CI | gl-{pipeline_id} | gl-987654 |
| Jenkins | jenkins-{build_id} | jenkins-42 |
| Local dev | local-{username}-{random} | local-alice-a1b2c3 |
| Test class | {ci_prefix}-{test_class} | gha-12345-test_signup |