CI/CD Integration
Integrating DevTools into your CI/CD pipeline enables automated API testing, continuous validation, and deployment gatekeeping. This guide covers integration with popular CI/CD platforms and best practices.
Why API Testing in CI/CD?
Automated API tests in CI/CD provide:
- Continuous Validation: Catch regressions before deployment
- Deployment Confidence: Verify APIs work before releasing
- Documentation: Tests serve as executable API documentation
- Environment Verification: Validate staging/production environments
- Faster Feedback: Detect issues within minutes, not hours or days
Quick Start
1. Export Your Flow to YAML
Studio Application:
- Open your workspace
- Right-click on a flow → Export → YAML
- Save as
api-tests.yaml - Commit to your repository
git add api-tests.yaml
git commit -m "Add API test flow"
git push2. Add CI Configuration
Create a CI workflow file (platform-specific examples below).
3. Set Environment Variables
Configure secrets in your CI platform for sensitive values like API keys.
4. Run Tests
The CI system will execute your tests on every commit, pull request, or deployment.
GitHub Actions
Basic Workflow
Create .github/workflows/api-tests.yml:
name: API Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install DevTools CLI
run: curl -fsSL https://sh.dev.tools/install.sh | bash
- name: Run API Tests
env:
API_BASE_URL: https://api-staging.example.com
API_KEY: ${{ secrets.API_KEY }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
run: |
devtools flow run api-tests.yaml \
--report junit:test-results.xml \
--report json:test-results.json
- name: Publish Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: |
test-results.xml
test-results.json
- name: Publish JUnit Report
uses: dorny/test-reporter@v1
if: always()
with:
name: API Test Results
path: test-results.xml
reporter: java-junitAdvanced: Matrix Testing
Test against multiple environments:
name: API Tests - Multi-Environment
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
environment:
- name: Staging
url: https://api-staging.example.com
- name: Production
url: https://api.example.com
steps:
- uses: actions/checkout@v4
- name: Install DevTools CLI
run: curl -fsSL https://sh.dev.tools/install.sh | bash
- name: Run Tests - ${{ matrix.environment.name }}
env:
API_BASE_URL: ${{ matrix.environment.url }}
API_KEY: ${{ secrets.API_KEY }}
run: |
devtools flow run api-tests.yaml \
--report junit:test-results-${{ matrix.environment.name }}.xml
- name: Upload Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ matrix.environment.name }}
path: test-results-${{ matrix.environment.name }}.xmlDeployment Gate
Only deploy if API tests pass:
name: Deploy with API Tests
on:
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install DevTools CLI
run: curl -fsSL https://sh.dev.tools/install.sh | bash
- name: Run API Tests
env:
API_BASE_URL: https://api-staging.example.com
API_KEY: ${{ secrets.STAGING_API_KEY }}
run: devtools flow run smoke-tests.yaml
deploy:
needs: test # Only runs if test job succeeds
runs-on: ubuntu-latest
steps:
- name: Deploy to production
run: ./deploy.shGitLab CI
Basic Pipeline
Create .gitlab-ci.yml:
stages:
- test
- deploy
api_tests:
stage: test
image: ubuntu:latest
before_script:
- apt-get update && apt-get install -y curl
- curl -fsSL https://sh.dev.tools/install.sh | bash
- export PATH="/usr/local/bin:$PATH"
script:
- |
devtools flow run api-tests.yaml \
--report junit:test-results.xml \
--report json:test-results.json
variables:
API_BASE_URL: https://api-staging.example.com
API_KEY: $CI_API_KEY
artifacts:
when: always
reports:
junit: test-results.xml
paths:
- test-results.json
deploy_production:
stage: deploy
dependencies:
- api_tests
script:
- ./deploy.sh
only:
- mainEnvironment-Specific Tests
test_staging:
stage: test
script:
- devtools flow run api-tests.yaml
variables:
API_BASE_URL: https://api-staging.example.com
API_KEY: $STAGING_API_KEY
only:
- develop
test_production:
stage: test
script:
- devtools flow run smoke-tests.yaml
variables:
API_BASE_URL: https://api.example.com
API_KEY: $PROD_API_KEY
only:
- mainJenkins
Declarative Pipeline
Create Jenkinsfile:
pipeline {
agent any
environment {
API_BASE_URL = 'https://api-staging.example.com'
API_KEY = credentials('api-key-staging')
}
stages {
stage('Install DevTools CLI') {
steps {
sh 'curl -fsSL https://sh.dev.tools/install.sh | bash'
}
}
stage('Run API Tests') {
steps {
sh '''
devtools flow run api-tests.yaml \
--report junit:test-results.xml \
--report json:test-results.json
'''
}
}
stage('Publish Results') {
steps {
junit 'test-results.xml'
archiveArtifacts artifacts: 'test-results.json', allowEmptyArchive: true
}
}
}
post {
always {
cleanWs()
}
failure {
emailext(
subject: "API Tests Failed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: "Check console output at ${env.BUILD_URL}",
to: 'team@example.com'
)
}
}
}Multibranch Pipeline
Test different environments based on branch:
pipeline {
agent any
environment {
API_BASE_URL = getApiUrl()
API_KEY = credentials(getApiKeyCredentialId())
}
stages {
stage('API Tests') {
steps {
sh 'curl -fsSL https://sh.dev.tools/install.sh | bash'
sh 'devtools flow run api-tests.yaml --report junit:results.xml'
}
}
}
post {
always {
junit 'results.xml'
}
}
}
def getApiUrl() {
switch(env.BRANCH_NAME) {
case 'main':
return 'https://api.example.com'
case 'develop':
return 'https://api-staging.example.com'
default:
return 'https://api-dev.example.com'
}
}
def getApiKeyCredentialId() {
switch(env.BRANCH_NAME) {
case 'main':
return 'api-key-production'
case 'develop':
return 'api-key-staging'
default:
return 'api-key-dev'
}
}CircleCI
Create .circleci/config.yml:
version: 2.1
jobs:
api-tests:
docker:
- image: cimg/base:stable
steps:
- checkout
- run:
name: Install DevTools CLI
command: curl -fsSL https://sh.dev.tools/install.sh | bash
- run:
name: Run API Tests
command: |
devtools flow run api-tests.yaml \
--report junit:test-results.xml \
--report json:test-results.json
environment:
API_BASE_URL: https://api-staging.example.com
- store_test_results:
path: test-results.xml
- store_artifacts:
path: test-results.json
workflows:
test-and-deploy:
jobs:
- api-tests
- deploy:
requires:
- api-tests
filters:
branches:
only: mainAzure Pipelines
Create azure-pipelines.yml:
trigger:
- main
- develop
pool:
vmImage: 'ubuntu-latest'
variables:
- group: api-secrets # Variable group with API_KEY
steps:
- checkout: self
- bash: curl -fsSL https://sh.dev.tools/install.sh | bash
displayName: 'Install DevTools CLI'
- bash: |
devtools flow run api-tests.yaml \
--report junit:test-results.xml \
--report json:test-results.json
displayName: 'Run API Tests'
env:
API_BASE_URL: https://api-staging.example.com
API_KEY: $(API_KEY)
- task: PublishTestResults@2
displayName: 'Publish Test Results'
condition: always()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: 'test-results.xml'
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifacts'
condition: always()
inputs:
pathToPublish: 'test-results.json'
artifactName: 'test-results'Travis CI
Create .travis.yml:
language: minimal
branches:
only:
- main
- develop
env:
global:
- API_BASE_URL=https://api-staging.example.com
secure:
# Encrypted API_KEY (use: travis encrypt API_KEY=your-key)
- secure: "encrypted_api_key_here"
before_install:
- curl -fsSL https://sh.dev.tools/install.sh | bash
- export PATH="/usr/local/bin:$PATH"
script:
- devtools flow run api-tests.yaml --report junit:results.xml
after_script:
- cat results.xmlDocker Integration
Dockerfile for Tests
Create a container image with DevTools CLI:
FROM alpine:latest
# Install dependencies
RUN apk add --no-cache curl bash
# Install DevTools CLI
RUN curl -fsSL https://sh.dev.tools/install.sh | bash
# Copy test files
COPY api-tests.yaml /tests/
WORKDIR /tests
# Run tests
CMD ["devtools", "flow", "run", "api-tests.yaml", "--report", "junit:results.xml"]Build and run:
# Build
docker build -t api-tests .
# Run
docker run -e API_BASE_URL=https://api.example.com \
-e API_KEY=secret \
api-testsDocker Compose
Test with dependent services:
version: '3.8'
services:
api:
image: mycompany/api:latest
environment:
- DB_HOST=db
ports:
- "8080:8080"
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: password
api-tests:
build: .
depends_on:
- api
- db
environment:
API_BASE_URL: http://api:8080
DB_PASSWORD: passwordRun:
docker-compose up --exit-code-from api-testsBest Practices
1. Separate Test Types
Organize flows by test type:
tests/
├── smoke-tests.yaml # Quick health checks (2-3 min)
├── regression-tests.yaml # Full test suite (10-15 min)
├── integration-tests.yaml # Cross-service tests
└── performance-tests.yaml # Load/stress testsCI Strategy:
- Run smoke tests on every commit
- Run regression tests on PRs
- Run integration tests pre-deployment
- Run performance tests nightly
2. Use Environment-Specific Secrets
Never hardcode secrets:
# ✅ Good
env:
API_KEY: '#env:SECRET_API_KEY'
# ❌ Bad
env:
API_KEY: 'sk_live_abc123'Configure in CI:
GitHub Actions:
Settings → Secrets → New repository secret
Name: API_KEY
Value: sk_live_abc123GitLab CI:
Settings → CI/CD → Variables → Add Variable
Key: CI_API_KEY
Value: sk_live_abc123
Protected: Yes
Masked: Yes3. Generate Multiple Report Formats
Always output both console and file reports:
devtools flow run tests.yaml \
--report console \
--report junit:results.xml \
--report json:results.jsonBenefits:
- Console for debugging CI logs
- JUnit for CI test reporting
- JSON for custom analysis
4. Fail Fast on Critical Tests
Use separate jobs for critical vs. comprehensive tests:
jobs:
smoke_tests:
# Quick, critical tests (1-2 min)
# Fail fast if these don't pass
steps:
- run: devtools flow run smoke-tests.yaml
full_tests:
needs: smoke_tests # Only run if smoke tests pass
# Comprehensive test suite (10-15 min)
steps:
- run: devtools flow run regression-tests.yaml5. Test Against Staging First
Always validate against staging before production:
test_staging:
# Test staging environment
environment:
API_BASE_URL: https://api-staging.example.com
deploy_production:
needs: test_staging
# Deploy to production only if staging tests pass6. Cache CLI Binary
Speed up CI by caching the CLI binary:
GitHub Actions:
- name: Cache DevTools CLI
uses: actions/cache@v4
with:
path: /usr/local/bin/devtools
key: devtools-cli-${{ runner.os }}-v0.5.1
- name: Install DevTools CLI
if: steps.cache.outputs.cache-hit != 'true'
run: curl -fsSL https://sh.dev.tools/install.sh | bash7. Set Timeouts
Prevent hanging tests:
- name: Run API Tests
timeout-minutes: 10 # Kill after 10 minutes
run: devtools flow run tests.yamlIn YAML flows:
variables:
- name: timeout
value: '30' # 30 second timeout per request8. Archive Test Artifacts
Always save test results:
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always() # Upload even if tests fail
with:
name: test-results
path: |
test-results.xml
test-results.jsonMonitoring and Alerts
Slack Notifications
GitHub Actions:
- name: Notify Slack
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "API Tests Failed: ${{ github.repository }} - ${{ github.ref }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "❌ *API Tests Failed*\nRepository: ${{ github.repository }}\nBranch: ${{ github.ref }}\nCommit: ${{ github.sha }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}Email Alerts
Jenkins:
post {
failure {
emailext(
subject: "API Tests Failed: ${env.JOB_NAME}",
body: """
API tests have failed.
Job: ${env.JOB_NAME}
Build: ${env.BUILD_NUMBER}
URL: ${env.BUILD_URL}
""",
to: 'team@example.com'
)
}
}Metrics Collection
Parse JSON reports for custom metrics:
# Extract metrics from JSON report
cat test-results.json | jq '{
total_duration: .duration,
total_nodes: (.nodes | length),
failures: (.nodes | map(select(.state == "failure")) | length)
}'Troubleshooting
Tests Pass Locally but Fail in CI
Possible Causes:
- Environment variable differences
- Network/firewall restrictions
- Different base URL
- Timing issues (CI may be slower)
Solutions:
- Print environment variables for debugging:
env | grep API devtools flow run tests.yaml - Increase timeouts for CI:
variables: - name: timeout value: '60' # Higher for CI - Use same environment URLs
CLI Not Found in CI
Error: devtools: command not found
Solution: Ensure PATH includes install directory:
export PATH="/usr/local/bin:$PATH"
devtools versionTests Hang in CI
Causes:
- Long-running requests
- Network timeouts
- Infinite loops
Solutions:
- Set job timeout:
timeout-minutes: 10 - Set request timeout:
variables: - name: timeout value: '30' - Add debug logging:
LOG_LEVEL=DEBUG devtools flow run tests.yaml
Secrets Not Loading
Error: Variable 'API_KEY' not found
Check:
- Secret is defined in CI platform
- Secret name matches exactly (case-sensitive)
- Secret is exposed as environment variable:
env: API_KEY: ${{ secrets.API_KEY }} - YAML uses
#env:reference:env: API_KEY: '#env:API_KEY'
Next: Explore Examples & Best Practices for real-world usage patterns and advanced techniques.