Newman in CI/CD Pipelines — GitHub Actions and Jenkins

9 min read

A test that only runs when you remember to click "Run" isn't really a test. CI/CD is what makes API tests load-bearing: every push, every deploy, every merge automatically runs the suite, and a failure blocks the change from shipping. This lesson takes the Newman command from the previous lesson and wires it into the two most common CI systems — GitHub Actions and Jenkins. The masterclass API Testing in CI/CD lesson covered the strategy; this lesson is the Postman-flavoured plumbing.

What "wired into CI" actually means

By the end of this lesson, every time someone pushes a branch:

  1. The CI worker checks out the code, including a postman/ folder containing the collection JSON.
  2. It installs Newman.
  3. It runs the collection against staging (or whichever target makes sense).
  4. It generates an HTML report and (optionally) JUnit XML.
  5. If any assertion failed, the CI step fails — which fails the build, which blocks the PR from merging.
  6. The HTML report is uploaded as a build artifact for triage.

The pipeline does what a tester would do, every time, with no human prompting. That's the point.

The CI pipeline, step by step

Step 1 of 6

Push triggers the workflow

Developer pushes a branch or opens a PR. GitHub Actions / Jenkins detects the event and queues a job.

Six steps. Each one is a few lines of YAML or Groovy. Total config: 30-50 lines. The setup pays for itself within a week.

What lives in your repo

A clean shape that scales:

your-repo/
├── postman/
│   ├── api-tests.postman_collection.json
│   ├── staging.postman_environment.json
│   └── README.md           ← how to run locally
├── package.json            ← Newman pinned as devDependency
├── .github/workflows/
│   └── api-tests.yml       ← GitHub Actions workflow
└── (the rest of your code)

Three principles:

  • The collection JSON is committed. It's the source of truth. CI doesn't fetch from Postman's cloud at run time.
  • Environment files are sanitised. Initial values are placeholders or empty strings. Real values come from CI secrets.
  • Newman is pinned. package.json lists newman and newman-reporter-htmlextra as devDependencies so versions don't drift between machines.

GitHub Actions — the YAML

Create .github/workflows/api-tests.yml:

name: API Tests
on:
  push:
    branches: [main, develop]
  pull_request:
 
jobs:
  api-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Check out
        uses: actions/checkout@v4
 
      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
 
      - name: Install Newman + htmlextra
        run: npm install -g newman newman-reporter-htmlextra
 
      - name: Run API tests
        run: |
          newman run postman/api-tests.postman_collection.json \
            -e postman/staging.postman_environment.json \
            --env-var "apiKey=${{ secrets.STAGING_API_KEY }}" \
            --env-var "baseUrl=${{ secrets.STAGING_BASE_URL }}" \
            --reporters cli,htmlextra,junit \
            --reporter-htmlextra-export reports/api-report.html \
            --reporter-junit-export reports/results.xml \
            --timeout-request 10000
 
      - name: Upload report
        if: always()    # upload even on failure
        uses: actions/upload-artifact@v4
        with:
          name: api-test-report
          path: reports/

A few details worth noticing:

  • on: push / pull_request — runs on every branch push and every PR. You can narrow to specific branches if the suite is slow.
  • secrets.STAGING_API_KEY — pulled from GitHub repo secrets (Settings → Secrets and variables → Actions). Never goes through committed files.
  • --env-var overrides values in the env JSON, so the file can ship empty placeholders.
  • if: always() on the artifact upload — without it, a failed Newman step skips the upload and you can't see the report. With it, the report is always available for triage.
  • --timeout-request 10000 — a 10s per-request timeout protects against hung requests stalling the build.

When the workflow runs, the job log shows the CLI output live. The artifact tab on the run gives you the HTML report. A failed assertion fails the step, which fails the workflow, which (if branch protection is configured) blocks the PR from merging.

Jenkins — the Jenkinsfile

For Jenkins, the equivalent declarative pipeline:

pipeline {
    agent any
 
    options {
        timeout(time: 15, unit: 'MINUTES')
    }
 
    environment {
        STAGING_API_KEY = credentials('staging-api-key')
    }
 
    stages {
        stage('Setup') {
            steps {
                sh 'npm install -g newman newman-reporter-htmlextra'
            }
        }
 
        stage('API Tests') {
            steps {
                sh '''
                    newman run postman/api-tests.postman_collection.json \
                      -e postman/staging.postman_environment.json \
                      --env-var "apiKey=$STAGING_API_KEY" \
                      --reporters cli,htmlextra,junit \
                      --reporter-htmlextra-export reports/api-report.html \
                      --reporter-junit-export reports/results.xml \
                      --timeout-request 10000
                '''
            }
        }
    }
 
    post {
        always {
            junit allowEmptyResults: true, testResults: 'reports/results.xml'
            archiveArtifacts artifacts: 'reports/**', allowEmptyArchive: true
        }
        failure {
            slackSend channel: '#qa-alerts',
                      color: 'danger',
                      message: "API tests failed: ${env.BUILD_URL}"
        }
    }
}

Notes:

  • credentials('staging-api-key') — pulls from Jenkins's Credentials store. The variable becomes a masked env var.
  • junit testResults: 'reports/results.xml' — the JUnit reporter from the previous lesson now feeds Jenkins's test dashboard. Failing tests show up in the build's "Tests" tab.
  • archiveArtifacts — uploads reports/ as a downloadable artifact.
  • post { always } — the same "always upload" pattern as GitHub Actions, just in Groovy.
  • post { failure } — Slack notification on failure (drop in if you don't want this).

GitLab CI, CircleCI, ArgoCD, and others follow the same shape — checkout, install Newman, run, upload report. The mechanics are identical; only the YAML/DSL differs.

Secrets management — the absolute rules

Three rules that hold across every CI system:

  1. Never commit a real secret to Git. Once it's in the history, rotation is the only safe response. Audit your environment JSON before committing — Initial values get exported too.
  2. Pull secrets from CI's secret store at runtime. GitHub Actions secrets, Jenkins Credentials, GitLab CI variables, AWS Secrets Manager — pick one. Inject as environment variables; reference in Newman with --env-var "key=$VAR".
  3. Mask secrets in logs. Most CI systems automatically mask configured secrets in their logs. Verify this — if your STAGING_API_KEY shows up plaintext in a build log, the masking config is wrong.

A safe environment file looks like this — placeholders for sensitive values, real defaults for non-sensitive ones:

{
  "name": "Staging",
  "values": [
    { "key": "baseUrl", "value": "https://staging.api.example.com", "type": "default", "enabled": true },
    { "key": "apiKey", "value": "", "type": "secret", "enabled": true },
    { "key": "testUserId", "value": "1", "type": "default", "enabled": true }
  ]
}

apiKey is empty in Git; CI fills it via --env-var.

When to run, and which suite

Your collection probably contains a mix — fast smoke tests, broader functional tests, slow integration cases. Match the suite to the trigger:

  • Every PR — the smoke folder (under 1 minute). Fast feedback, gates merging.
  • On merge to main — the full functional suite (5–10 minutes).
  • Nightly — the full suite plus integration tests, plus longer data-driven runs.
  • Pre-deploy — smoke against staging before the deploy proceeds.
  • Post-deploy — smoke against production (read-only paths only) to confirm the deploy didn't break anything obvious.

Use Newman's --folder flag to slice the same collection into different suites:

newman run collection.json --folder Smoke -e staging.json
newman run collection.json --folder Functional -e staging.json
newman run collection.json --folder Smoke -e prod.json

One collection, many CI jobs, each appropriate to its trigger.

Notifications and gating

A few patterns that hold up:

  • Branch protection rules. In GitHub: Settings → Branches → require the api-tests check before merging. PR can't merge with red API tests.
  • Required checks. Same idea in GitLab and Bitbucket — your API test job becomes a required pipeline step.
  • Slack on red. Use the JSON reporter (previous lesson) and a small script, or use a community Slack action. Default to "notify on red, silent on green" — a green-build flood becomes ignorable.
  • Auto-rollback (advanced). Run smoke after a deploy; if it fails, trigger a rollback step. Pairs well with deploy systems like ArgoCD.

⚠️ Common mistakes

  • Putting Newman runs only in nightly jobs. A nightly suite catches bugs 24 hours after they're introduced. Run smoke on every PR; nightly is for the long tail. The shorter the loop, the cheaper the fix.
  • Ignoring artifact retention. CI artifact stores fill up. Set retention policies (GitHub Actions defaults to 90 days; tighten if you're producing a report per push).
  • Hitting production in CI without read-only safeguards. A misconfigured CI run with destructive POST/DELETE requests against prod is a textbook horror. Use a separate prod-smoke collection or folder containing only read-only requests.

🎯 Practice task

Wire your suite into CI. 30-45 minutes.

  1. Set up the repo structure. In a fresh Git repo (or a sandbox folder), make a postman/ directory. Drop your exported JSONPlaceholder API Tests.postman_collection.json and JSONPlaceholder.postman_environment.json into it.
  2. Initialise npm and pin Newman:
    npm init -y
    npm install --save-dev newman newman-reporter-htmlextra
    Add to package.json:
    "scripts": {
      "test:api": "newman run postman/JSONPlaceholder\\ API\\ Tests.postman_collection.json -e postman/JSONPlaceholder.postman_environment.json -r cli,htmlextra,junit --reporter-htmlextra-export reports/api-report.html --reporter-junit-export reports/results.xml"
    }
    Run npm run test:api locally. Confirm reports/api-report.html and reports/results.xml are generated.
  3. Add to Git. Commit postman/, package.json, package-lock.json. Add reports/ and node_modules/ to .gitignore. Push.
  4. Create a GitHub Actions workflow. Make .github/workflows/api-tests.yml with the YAML above (adjust paths to match your file names). Push. Open the Actions tab in GitHub — the workflow should run on the push and (assuming everything works) finish green.
  5. Inspect the artifact. Click the workflow run → scroll to Artifacts → download api-test-report. Open the HTML in your browser. Same report you got locally, generated by CI.
  6. Force a failure in CI. Edit one of the assertions in Postman (expect(...).to.equal(999)), export, commit, push. Watch the workflow fail. Open the artifact — the HTML report shows the failed assertion clearly.
  7. Branch protection (optional). In your repo settings, require the api-tests check on the default branch. Try to merge a PR with failing tests — GitHub blocks the merge.
  8. Stretch: add a smoke job that runs only the smoke folder on every PR, plus a nightly job (schedule: cron: "0 2 * * *") that runs the full suite. Two YAML files, one collection, three different test cadences.

That ends Chapter 5. You can now run a Postman collection from the GUI, the command line, and an automated CI/CD pipeline — every push protected by the same suite. Chapter 6 covers the parts of Postman that aren't about running tests but about collaborating on them: Postman Flows, auto-generated documentation, and team workspace features.

// tip to track lessons you complete and pick up where you left off across devices.