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.
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 moment | Suite | Question it answers | Target | Typical time |
|---|---|---|---|---|
| Pull request changing the API or its contract | Contract and field validation | Did this change break the contract? | Ephemeral or dev environment | Minutes |
| After deployment to a test environment | Smoke (tagged) | Is the deployment alive and safe? | SIT | Under 2 minutes |
| Nightly | Full regression, including lifecycle and async flows | Does everything still work together? | SIT | Longer is acceptable |
| Before release | Regression plus security authorization checks | Is this build fit to promote? | UAT or pre-production | Agreed with release manager |
| After production deployment | Read-only health checks only | Is production responding? | Production | Seconds |
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:
| Option | Use in CI |
|---|---|
--env sit | Select the environment file |
--env-var name=value | Inject a secret or CI value at runtime; repeatable |
--tags smoke / --exclude-tags slow | Run by purpose |
--bail | Stop on first failure, useful for smoke gates |
--json-file-path / --csv-file-path | Data-driven runs |
--reporter-junit, --reporter-html | Results for the pipeline UI and a human-readable report |
--reporter-skip-headers, --reporter-skip-body | Keep credentials and payloads out of uploaded reports |
--cacert | Trust a corporate CA certificate |
--client-cert-config | Client certificates for mutual TLS endpoints |
--sandbox=developer | Only 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:
pathslimits pull request runs to changes that can affect the API.--env-varinjects credentials from GitHub secrets, which GitHub masks in logs. The committedsit.ymlholds no secret values.runIdfrom the workflow run ID makes test data unique per run and traceable back to the pipeline.--reporter-skip-headers Authorizationkeeps 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.
| Situation | Approach |
|---|---|
| Test environment is internal only | Self-hosted runner inside the network, registered to the repository or organization |
| Corporate proxy inspects TLS | Pass the corporate CA with --cacert, and configure the proxy for the runner |
| API requires mutual TLS | Client certificate from the secret store, referenced with --client-cert-config |
| API allowlists caller IPs | Allowlist the self-hosted runner, not individual laptops |
| Credentials come from a vault | Fetch 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:
| Symptom | Cause | Fix |
|---|---|---|
| Fails when two pipelines run at once | Shared, hardcoded test data | Unique data per run from runId; never reuse references |
| Fails on busy days | Fixed sleep shorter than async processing | Capped polling on status, as in Part 6 |
| Fails only in the full run | Suite depends on data another suite created | Each suite creates what it needs in its own setup |
| Fails after an hour | Token expired mid-run | Refresh tokens in a collection-level pre-request script |
| Fails in parallel matrix runs | Rate limits | Stagger jobs, use --delay, or separate credentials per job |
Fails randomly with 5xx | Unstable environment | Report 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.
Related articles
- Chaining API Requests With JavaScript in Bruno and Postman: The Scripts Analysts Need Chain API requests in Bruno and Postman: capture values, pre-request and post-response scripts, token refresh, polling, branching, and a Stripe sandbox flow.
- How to Write API Test Cases: 40 Tests Derived From One Endpoint How to write API test cases from the contract: a six-source derivation method, 40 worked cases for one payment endpoint, and data-driven automation in Bruno.
- Contract Testing: Catch Breaking Changes Before They Ship What contract testing is, how it differs from integration testing, and how consumer-driven contracts catch breaking API and event changes before they reach production.
- Smoke, Sanity, and Regression Testing: What Each One Proves Smoke testing proves the build is testable, sanity testing proves a fix landed, regression testing proves nothing else broke. How the three differ and when each runs.
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.