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:

  1. Store your API key as a CI secret.
  2. Set the ONESHOT_API_KEY environment variable.
  3. Run your tests normally.
  4. 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:

ContextLabel formatExample
GitHub Actionsgha-{run_id}gha-12345678
GitLab CIgl-{pipeline_id}gl-987654
Jenkinsjenkins-{build_id}jenkins-42
Local devlocal-{username}-{random}local-alice-a1b2c3
Test class{ci_prefix}-{test_class}gha-12345-test_signup