>_ Analyst Engineering
QA AnalystDeveloper Analyst Follow

Running API Tests in CI: Bruno CLI and Newman in GitHub Actions

Written by Ahmed at Analyst Engineering, a Senior Technical Business Analyst with 10+ years in banking and payments delivery.

Cover for running API tests in CI, showing a GitHub Actions workflow running bru run with tags and publishing JUnit and HTML reports.

Key takeaways

  • Running an API collection in CI turns it from a personal tool into a gate: the Bruno CLI and Newman both exit with a non-zero code when an assertion fails, which is all a pipeline needs to block a merge or a release.
  • Match suites to pipeline moments: a tagged smoke subset after every deployment, contract and regression suites on pull requests or nightly, and never write-heavy suites against production.
  • Secrets reach a CI run from the platform's secret store at runtime, for example through the Bruno CLI's env-var option or Newman's env-var option, never from committed environment files.
  • GitHub-hosted runners cannot reach internal test environments behind a corporate network, so bank and enterprise API suites usually need self-hosted runners inside the network, plus the corporate CA certificate passed to the CLI.
  • Flaky API tests are fixed at the cause: unique data per run, capped polling instead of fixed sleeps, no dependence on other suites' data, and retries only for network errors, never for failed assertions.

Running API tests in CI means a collection runs automatically on every change or deployment and blocks the pipeline when an assertion fails. It is the step that turns an analyst’s Bruno or Postman collection from a personal tool into a team safety net. Both the Bruno CLI and Newman do it with one command; the real work is choosing what runs when, handling secrets and networks, and keeping the suite trustworthy.

To run API tests in CI: commit the collection to the repository; decide which suites run at which pipeline moment; install the Bruno CLI or Newman in the pipeline; run the suite against a named environment with secrets injected from the CI secret store; publish JUnit results for the pipeline UI and an HTML report as an artifact; and let the non-zero exit code on failure block the merge or release. For internal APIs, run on a self-hosted runner inside the network.

APIs for Analysts, advanced track. Builds on Part 5, API test cases and Part 6, chaining with JavaScript. Full learning path: APIs for Analysts.

The examples use GitHub Actions because its syntax is readable and widely used; the same commands run in GitLab CI, Azure DevOps, Jenkins, or Bitbucket Pipelines. The test design that makes a suite worth gating on is the subject of API Testing and QA Mastery for BAs.

Which API tests should run at which point in the pipeline?

Not every suite belongs on every commit. Match each suite to the question the pipeline is asking at that moment.

Pipeline momentSuiteQuestion it answersTargetTypical time
Pull request changing the API or its contractContract and field validationDid this change break the contract?Ephemeral or dev environmentMinutes
After deployment to a test environmentSmoke (tagged)Is the deployment alive and safe?SITUnder 2 minutes
NightlyFull regression, including lifecycle and async flowsDoes everything still work together?SITLonger is acceptable
Before releaseRegression plus security authorization checksIs this build fit to promote?UAT or pre-productionAgreed with release manager
After production deploymentRead-only health checks onlyIs production responding?ProductionSeconds

The last row has one rule with no exceptions: no suite that creates, changes, or deletes data runs against production. Production checks are read-only health and version calls. How smoke, sanity, and regression suites relate is laid out in smoke, sanity, and regression testing.

How should the repository be organized for CI?

Keep the collection next to the API it tests, so a contract change and its test change travel in the same pull request.

payments-api/
├── src/
├── openapi.yaml
├── api-tests/
│   ├── opencollection.yml
│   ├── .env.sample
│   ├── environments/
│   │   ├── local.yml
│   │   └── sit.yml
│   ├── data/
│   │   └── field-validation.json
│   ├── 10-smoke/
│   ├── 20-contract/
│   ├── 30-lifecycle/
│   └── 40-security/
└── .github/
    └── workflows/
        └── api-tests.yml

Two conventions make CI simpler. Tag requests such as smoke, regression, and release-gate, so pipelines select by purpose rather than by folder. And keep every environment file free of secret values: secrets are referenced as variables and injected at runtime.

How do you run Bruno tests in GitHub Actions?

The Bruno CLI runs a collection, a folder, several folders, or requests filtered by tags. The options that matter in CI:

OptionUse in CI
--env sitSelect the environment file
--env-var name=valueInject a secret or CI value at runtime; repeatable
--tags smoke / --exclude-tags slowRun by purpose
--bailStop on first failure, useful for smoke gates
--json-file-path / --csv-file-pathData-driven runs
--reporter-junit, --reporter-htmlResults for the pipeline UI and a human-readable report
--reporter-skip-headers, --reporter-skip-bodyKeep credentials and payloads out of uploaded reports
--cacertTrust a corporate CA certificate
--client-cert-configClient certificates for mutual TLS endpoints
--sandbox=developerOnly if scripts need npm packages or file access; safe mode is the default since CLI v3

A workflow that runs the smoke suite on every pull request and the full regression nightly:

name: api-tests

on:
  pull_request:
    paths: ["openapi.yaml", "src/**", "api-tests/**"]
  schedule:
    - cron: "0 5 * * 1-5"
  workflow_dispatch:

permissions:
  contents: read

jobs:
  bruno:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v6

      - uses: actions/setup-node@v6
        with:
          node-version: "24"

      - name: Install Bruno CLI
        run: npm install -g @usebruno/cli

      - name: Run API tests
        working-directory: api-tests
        run: |
          mkdir -p results
          TAGS="smoke"
          if [ "${{ github.event_name }}" = "schedule" ]; then TAGS="regression"; fi
          bru run -r \
            --env sit \
            --tags "$TAGS" \
            --env-var clientId="${{ secrets.SIT_CLIENT_ID }}" \
            --env-var clientSecret="${{ secrets.SIT_CLIENT_SECRET }}" \
            --env-var runId="gh-${{ github.run_id }}" \
            --reporter-junit results/junit.xml \
            --reporter-html results/report.html \
            --reporter-skip-headers Authorization

      - name: Upload reports
        if: ${{ !cancelled() }}
        uses: actions/upload-artifact@v6
        with:
          name: api-test-reports-${{ github.run_id }}
          path: api-tests/results/

What each part does for you:

  • paths limits pull request runs to changes that can affect the API.
  • --env-var injects credentials from GitHub secrets, which GitHub masks in logs. The committed sit.yml holds no secret values.
  • runId from the workflow run ID makes test data unique per run and traceable back to the pipeline.
  • --reporter-skip-headers Authorization keeps tokens out of an artifact anyone with repository access can download.
  • if: ${{ !cancelled() }} uploads reports even when tests fail, which is exactly when you need them.

Bruno also publishes an official action, usebruno/bruno-cli-action@v1, which takes the same bru arguments in a command input and exposes passed, failed, and total counts as step outputs, handy for notifications or pull request comments.

How do you run a Postman collection in CI with Newman?

Newman is Postman’s command line runner. With the collection and environment exported to the repository:

      - name: Run Postman collection
        run: |
          npx newman run api-tests/payments.postman_collection.json \
            -e api-tests/sit.postman_environment.json \
            --folder "10-smoke" \
            --env-var "clientId=${{ secrets.SIT_CLIENT_ID }}" \
            --env-var "clientSecret=${{ secrets.SIT_CLIENT_SECRET }}" \
            -r cli,junit \
            --reporter-junit-export results/junit.xml

The trade-off is the one described in Bruno vs Postman for analysts: a Postman collection edited in the app must be re-exported to the repository for CI to see the change, so agree who exports and when, or the pipeline tests yesterday’s suite.

How do you run API tests against internal environments?

This is where enterprise pipelines differ from tutorials. GitHub-hosted runners live on the public internet; your SIT environment lives behind the corporate network. They cannot reach each other.

SituationApproach
Test environment is internal onlySelf-hosted runner inside the network, registered to the repository or organization
Corporate proxy inspects TLSPass the corporate CA with --cacert, and configure the proxy for the runner
API requires mutual TLSClient certificate from the secret store, referenced with --client-cert-config
API allowlists caller IPsAllowlist the self-hosted runner, not individual laptops
Credentials come from a vaultFetch at runtime; Bruno CLI also supports external secret managers through --secrets-env-file

Budget for this early. On bank programmes, runner access, network rules, and test credentials for CI usually take longer to arrange than writing the workflow, and they involve platform, network, and security teams.

How do you keep CI API tests from becoming flaky?

A flaky gate is worse than no gate, because the team learns to ignore red builds. Fix flakiness at its cause:

SymptomCauseFix
Fails when two pipelines run at onceShared, hardcoded test dataUnique data per run from runId; never reuse references
Fails on busy daysFixed sleep shorter than async processingCapped polling on status, as in Part 6
Fails only in the full runSuite depends on data another suite createdEach suite creates what it needs in its own setup
Fails after an hourToken expired mid-runRefresh tokens in a collection-level pre-request script
Fails in parallel matrix runsRate limitsStagger jobs, use --delay, or separate credentials per job
Fails randomly with 5xxUnstable environmentReport to environment owner; track environment availability separately from test results

And two rules for retries. Retry network errors, never assertions: an assertion that passes on the second try is a race condition or a data problem, and hiding it hides a real defect. Quarantine, do not delete: move a flaky test to a non-blocking tag with an owner and a date, fix the cause, and bring it back.

What should happen when the gate fails?

Decide it before the first red build, and write it down.

  • Who is notified: the pull request author for PR runs; the team channel for nightly runs.
  • What blocks: smoke and contract failures block merge and promotion; nightly regression failures create a triage item before the next release decision.
  • Who triages: the analyst or QA owner of the failing area reads the report first, because they know whether the expected result or the system is wrong.
  • Where evidence lives: the HTML report artifact, with the trace IDs from failing responses, feeds straight into a defect using the approach in defect triage for analysts.

The analyst’s role in CI is not writing YAML. It is owning what the suite covers, keeping assertions tied to requirements, tagging tests by purpose, and making sure a red build means something the business would care about.

The APIs for Analysts learning path

Beginner: What is an API · API glossary · JSON for analysts · HTTP status codes · Your first collection · Why did my API request fail? · Reading an API contract

Intermediate: Analyze an API · Document an API · API test cases · Chaining and scripts · Webhooks · GraphQL

Advanced: POCs and demos · API design review · Versioning and breaking changes · API security testing · API tests in CI (you are here)

The takeaway

Running API tests in CI turns a collection into a gate, because the Bruno CLI and Newman fail the pipeline when an assertion fails. Match suites to moments: tagged smoke after deployment, contract checks on pull requests, full regression nightly, and only read-only checks in production. Inject secrets from the CI secret store, keep them out of reports, and plan early for self-hosted runners, corporate certificates, and mutual TLS on internal environments. Keep the gate trustworthy by fixing flakiness at its cause, retrying only network errors, and deciding in advance who triages a red build.

For the test design that deserves to gate a release, see API Testing and QA Mastery for BAs, and for pipelines that validate events as well as APIs, Automate Kafka Validation with Postman. Want help planning a CI test strategy for your programme? Book a 1:1 Tech BA Coaching Call.

Ahmed is a Senior Technical Business Analyst with 10+ years in banking and payments. He builds practical guides and tools for analysts at The Tech BA Toolkit.

Tags: CI/CD, API Testing, Bruno, Newman, GitHub Actions

About the author

Analyst Engineering is written by Ahmed, a Senior Technical Business Analyst with 10+ years of banking and payments delivery experience: ISO 20022 and SWIFT messaging, payments API integration, Kafka event validation, and production support. Every article comes from real delivery work, and each one is reviewed and updated as tools and standards change.

Free account

Practice on the Labs, keep your progress

A free account, no password: an email link signs you in. It saves your steps and self-assessments on the Labs, shows your missions on a dashboard, unlocks the solutions, and, if you tick the box, sends you new missions and articles when they ship.

Your email is used to sign you in. Nothing else, unless you ask. Privacy.