Connect your CI pipeline to Nexus
Configure Nexus once, add an upload step to your existing pipeline, then check the first result in Jira. Your tests continue to run in your CI environment. No Nexus plugin needs to be installed on your CI server.
You will need: a configured Nexus project, permission to manage its CI keys, access to your pipeline settings, and an existing test job. The upload examples use a Linux runner with Node.js 22 available. Keep your existing checkout, dependency installation and test steps.
If your tests do not produce a results file yet, use the automation setup guide first. If you already use the Nexus Playwright reporter or pytest plugin, give that test job the Nexus environment variables below; do not add a second upload for the same results.
Start a cycle in CI
Run in CI can start GitHub Actions, GitLab.com, Azure DevOps Pipelines or Bitbucket Pipelines directly from a Nexus cycle. A project administrator connects the target, branch, workflow or pipeline and access token once, then chooses which project, cycle, environment and build values to send. The pipeline runs your tests and returns results to that same cycle.
Tokens use Forge secret storage, are never shown again, and are deleted when a connection is removed. A double press within 60 seconds is blocked and every request appears in the audit log. Direct connections support cloud-hosted services; self-hosted Jenkins or GitLab uses the customer-hosted Nexus runner. See the service setup and example pipelines.
Configure Nexus
- Open your Jira project, then Project settings → Nexus Project Settings → CI.
- Create an API key with a recognisable label, such as
GitHub staging. Tick Send results for result uploads (the options are Send results, CI API and Runner). Copy the key now: it is shown only once. - Copy the results URL from the same page. This is the complete endpoint, not your Jira site address. Do not use the separate CI API URL for a results-file upload.
- Open an existing Test Case and copy its Automation Key. Put that key in the automated test's name, for example
NEX-123: customer can sign in. Use a case in this project that is ready to run, rather than an obsolete case. - Note your Jira project key and the environment you want the results associated with. The examples use
NEXandstaging; replace them with your values.
| Setting in your CI platform | Value to use | Where it comes from |
|---|---|---|
NEXUS_URL |
The complete results URL | Nexus project CI settings |
NEXUS_API_KEY |
The key you just created; store it as a secret | Nexus project CI settings |
--project NEX in the command |
Your Jira project key | Your Jira project |
--environment staging in the command |
Your target environment name or configured alias | Your test environment |
results.xml in the command |
The report file your job actually writes | Your test tool's reporter configuration |
The project key and report filename are ordinary configuration. The API key is a secret: never commit it, put it in a URL or print it in job logs.
Prepare your test report
The current development build has been verified with Playwright (Nexus reporter), Selenium with pytest (Nexus pytest plugin), Cypress (reporter or JUnit upload), Cucumber (JSON upload), JUnit (XML upload) and Mocha (mochawesome JSON upload). Robot Framework also works. Results match by Automation Key or issue key, record pass/fail per run, and failed runs can raise a linked Jira Bug.
A Jenkins job uses the same upload tool. Nexus can also queue Run automation
for the customer-hosted nexus-runner, which executes tests and posts results back.
Run one small test locally or in your existing pipeline. Configure its reporter to write a supported report file, and check that the file exists after the run. The automation guide gives the commands for each framework and shows where to place the Automation Key.
The snippets below upload results.xml. For Cucumber or Mocha JSON, replace that filename with your actual .json report. The upload CLI detects supported formats. If tests produce several XML files, upload each file separately; do not concatenate XML documents.
Run the upload CLI straight from npm with npx @resyncnz/nexus-upload (version 0.3.8). To use a file instead, download the upload CLI, save it as nexus-upload.mjs in the root of your test repository, and commit that file with your pipeline change. It has no package dependencies. Check the download against the published checksums if your organisation requires download verification.
Ready to continue: the pipeline checks out nexus-upload.mjs, has Node.js available, and produces a report with at least one test whose key matches an existing Nexus Test Case. Use fresh report output for each job so you cannot accidentally upload a stale file from an earlier run.
Configure your CI platform
Open the instructions for your platform. These are additions to an existing working test pipeline, not replacement pipelines. Run the uploader in the same job and working directory as the report; if you use a separate job, transfer the report as an artifact first.
GitHub Actions
Store the two secrets
In the repository, open Settings → Secrets and variables → Actions and create repository secrets named NEXUS_URL and NEXUS_API_KEY. Use the values copied from Nexus. Secrets may not be available to workflows from forked pull requests; start with a trusted branch in your own repository.
Add the upload step
Edit the workflow under .github/workflows/. Add this item to the existing job's steps, immediately after the test step. The job must already check out the repository and provide Node.js.
- name: Send test results to Nexus
if: ${{ always() }}
env:
NEXUS_URL: ${{ secrets.NEXUS_URL }}
NEXUS_API_KEY: ${{ secrets.NEXUS_API_KEY }}
run: node nexus-upload.mjs results.xml --project NEX --environment staging
always() allows the upload step to run after a test failure. An upload error fails this step; it does not convert a failed test job into a pass. If an earlier setup error prevented report creation, the uploader reports the missing file.
See GitHub's secret configuration and status conditions. Prefer the packaged action? Its installation instructions include where to place the downloaded action files.
GitLab CI
Store the two variables
Open Settings → CI/CD → Variables. Add NEXUS_URL and NEXUS_API_KEY; mask the key and hide it where that option is available. If you mark a variable protected, test from a protected branch or tag that is allowed to receive it.
Add the upload command
In .gitlab-ci.yml, add after_script to your existing test job. If it already has an after_script, append this command instead of creating a second block. Keep the existing job image, test script and dependency setup, and make sure Node.js is installed in that image.
after_script:
- node nexus-upload.mjs results.xml --project NEX --environment staging
GitLab runs after_script in a fresh shell, so define secrets in the CI/CD settings rather than relying on variables exported by the test script. The report remains in the job's working directory.
Check this job's upload log: an after_script failure does not change a successful job's exit status. A green pipeline is therefore not proof of successful delivery. Test failures remain failures. Timeouts and runner failures may prevent the upload from running.
Jenkins
Store the credentials
In the Jenkins credential store accessible to your pipeline, add two Secret text credentials. Set their IDs to nexus-url and nexus-api-key, containing the results URL and API key respectively.
Add the post-build upload
Inside your existing Declarative pipeline { ... }, add this post block after stages. If there is already a post { always { ... } } block, merge these steps into it. This example needs a Unix/Linux agent, Node.js and Jenkins Credentials Binding.
post {
always {
withCredentials([
string(credentialsId: 'nexus-url', variable: 'NEXUS_URL'),
string(credentialsId: 'nexus-api-key', variable: 'NEXUS_API_KEY')
]) {
sh 'node nexus-upload.mjs results.xml --project NEX --environment staging'
}
}
}
For Maven Surefire, replace the sh line with the following block. It checks that reports exist and uploads each XML file; it stops on an upload error.
sh '''
set -e
for report in target/surefire-reports/TEST-*.xml; do
test -f "$report" || { echo "No Surefire XML reports found"; exit 2; }
node nexus-upload.mjs "$report" --project NEX --environment staging
done
'''
Single-quoted Groovy strings let the uploader read credentials from the environment. Keep the test stage's original failure status; do not add || true to hide test failures. For Windows agents, use the corresponding bat or PowerShell step rather than sh.
Azure Pipelines
Store the variables
In your pipeline's Variables, add NEXUS_URL and NEXUS_API_KEY. Mark the key secret. If using a variable group, link it to the pipeline and authorise the pipeline to use it.
Add the upload step
In azure-pipelines.yml, append this item to the existing job's steps after the test step. Use a Linux agent with Node.js, and keep your current checkout and test setup.
- bash: node nexus-upload.mjs results.xml --project NEX --environment staging
displayName: Send test results to Nexus
condition: succeededOrFailed()
env:
NEXUS_URL: $(NEXUS_URL)
NEXUS_API_KEY: $(NEXUS_API_KEY)
Azure secret variables must be explicitly mapped into the script's environment. succeededOrFailed() runs the upload after success or failure, but not after cancellation. An upload error fails the step.
Bitbucket Pipelines
Store the repository variables
Open Repository settings → Pipelines → Repository variables. Add NEXUS_URL and NEXUS_API_KEY, marking the key Secured. If you use deployment variables instead, make sure the step is assigned to that deployment environment.
Add the upload command
In bitbucket-pipelines.yml, add this after-script to the same step that runs your tests, alongside its existing script. Append to an existing after-script block if it has one. The step image must have Node.js installed.
after-script:
- node nexus-upload.mjs results.xml --project NEX --environment staging
Bitbucket runs the command after the test script succeeds or fails. A failure in after-script does not change the step's status, so inspect its output and the Nexus delivery log. A green build alone is not delivery confirmation. Runner failures can prevent the command from running.
Verify the first delivery
- Run a small pipeline on a branch that can access the configured secrets.
- Confirm the test job produced the expected report. Open the Nexus upload step and check for an accepted response. HTTP 202 with
"status":"accepted"means Nexus processed every result before answering; check the delivery log for matched, unmatched or skipped outcomes. A 202 with"status":"partial"means send the same upload again. - Return to Project settings → Nexus Project Settings → CI and refresh the delivery log. Check the processed outcome: matched, unmatched or skipped results.
- Open the matched Test Case's execution history. Check its status, duration and target environment against your test output.
- In a safe test branch, run a deliberately failing test and confirm that its failure is also delivered. The CI test result must remain failed.
You are finished when: one known case has the expected recorded result, an intentional failure also reaches Nexus, and the team knows where to investigate delivery errors.
Troubleshooting
| What you see | What to check |
|---|---|
NEXUS_API_KEY missing |
Exact variable name, secret access for this branch/job, and the explicit env mapping on Azure or GitHub. |
401 or rejected key |
The key belongs to this project, has not been revoked and has the required permission. Create a replacement if necessary. |
Cannot find results.xml |
The report path, working directory and reporter setting. If tests never started, there may be no report. Transfer artifacts when uploading from another job. |
| HTTP 202, but no execution | Wait for processing, refresh the delivery log and inspect its matched/unmatched/skipped outcome. |
| Unmatched tests | The reported test names contain Automation Keys that exist in this Nexus project. Check the report itself, not just your source code. |
| Duplicate runs | Choose one uploader: reporter/plugin or a pipeline upload. Do not send the same report through both. |
400 |
The file is a supported report, not HTML, console output or several XML documents concatenated together. |
413 |
Check payload/result limits in the API reference. Split large reports; if the project is being rate-limited, reduce the upload rate. |
| Pipeline is green but no results arrive | Inspect upload output, especially GitLab and Bitbucket post-test commands, whose failures do not fail an otherwise successful job. |
For help, contact support with the platform, upload timestamp and error message. Do not include the API key.
Optional settings and next steps
Start with the project and environment. Once the first delivery works, add --test-set NEX-25 to record against a specific cycle, or --component web to identify a component. Replace example keys with your own. The CLI reads build/commit metadata from common CI environment variables; --build and --commit can override them.
Keep result-file ingestion separate from CI API operations such as recording a deployment: those use the CI API URL, an appropriate key scope and a JSON operation body. See the API reference for that configuration, request limits and all supported formats.
- Configure your automation framework
- Downloads and alternative integration helpers
- Review release readiness
Reference: uploading without the CLI, and every option
The steps above are all most teams need. This section is the full reference: sending
results with plain HTTP (curl) instead of the upload CLI, every query parameter and
response, the packaged reporters and plugins, other result formats, recording
deployments, reading data back, and runner troubleshooting.
Keep stack traces readable: plain curl loses line breaks (confirmed 28/09/2026). A raw
XML body uploaded with plain curl (Content-Type: application/xml, even with
--data-binary) loses every line break on the way into Nexus, so a stack trace or
<system-out> arrives as one long line. The results themselves (pass/fail, counts, keys)
are recorded correctly. Cause, confirmed: the Forge web trigger platform strips raw
newlines from the request body before Nexus's code receives it. A live check on dev
(Nexus 14.5.0) logged the same 11-line file as lf=0 on arrival
([ingestion] body received: chars=320 lf=0 cr=0 contentType=application/xml envelope=none),
while the same file sent through the CLI arrived as a base64 envelope and kept every break.
Nothing in Nexus can recover a break the platform has already removed.
What to use instead: the nexus-upload CLI, the nexus-runner, or the packaged
reporters and plugins (Playwright, pytest, Cypress, GitHub Action, Azure DevOps task). They
all send XML inside a small JSON upload envelope
({"format":"junit-xml","encoding":"base64","content":"<base64 of the file>"}), which keeps
the line breaks. To keep them with curl, send the envelope instead of the file:
printf '{"format":"junit-xml","encoding":"base64","content":"%s"}' "$(base64 < results.xml | tr -d '\r\n')" > envelope.json
curl --fail --silent --show-error -X POST "$NEXUS_URL?project=NEX" \
-H "Authorization: Bearer $NEXUS_API_KEY" -H "Content-Type: application/json" --data-binary @envelope.json
A skipped test is recorded as Not Applicable (28/09/2026), whatever the tool: Playwright, Mocha, Cypress, Cucumber (pending or skipped) and Newman used to record it as Blocked. Blocked now means the test could not run (for example a Cucumber step with no step definition, or an interrupted Playwright run).
Link each run to its CI job (28/09/2026). Add &buildUrl=<https URL of the job> and each
run shows Open CI job. The packaged integrations add it for you from the CI's own
variables.
Parallel shards: send the build id. Results for the same
test case share one test run only when they come from the same build (&build=, or
--build on nexus-upload) and the same tool (the result format, and &component=
when you send it). So shards of one test job in one pipeline run land on one run, whose
status is the worst of their results (a failed shard makes it Fail, whichever shard
finishes first), and every shard's results stay on it. Different builds, different tools
(Playwright, Robot, pytest, …) and different &component= values in the same pipeline each
get their own run. The pipeline snippets below already send the CI's build number; the packaged
integrations send it for you. Without a build id, uploads from the same tool arriving while
the run is still open share it, and other tools never do. To keep two jobs that use the
same tool apart, give each its own &component=.
What every snippet does: POST "$NEXUS_URL?project=<KEY>&environment=<ENV>&build=<BUILD>"
with Authorization: Bearer $NEXUS_API_KEY and the results file as the body. Nexus
answers 202 (accepted), 400, 401, 413 (over 5 MB) or 500, each with a small fixed
JSON body such as {"status":"accepted"} or {"status":"rejected","reason":"unauthorized"}
(the same for every request — it never carries per-result detail); curl --fail turns
anything but 2xx into a failed step. Every snippet has --retry 8 --retry-delay 5,
which resends on a 500, 502, 503, 504 or a timeout. Since the durable-ingestion
update deployed in development build 14.5.0 (28/09/2026),
{"status":"accepted"} is answered only after Nexus has processed every result of the
upload (nothing is left running behind it), and {"status":"partial"} means Nexus has
stored the upload and what it recorded so far and is finishing the rest itself in the
background; sending the same file again is safe and is how a pipeline confirms the end.
Optionally send an Idempotency-Key header (16–128 of A-Z a-z 0-9 . _ : -, e.g.
$BUILD_ID-results) with the same value on every send of one upload: a late resend is
then never recorded as a new run. curl does not resend a 202, so for large suites use one of the
packaged integrations below (they do it for you), or loop:
for i in 1 2 3 4 5 6 7 8; do
out=$(curl --fail --silent --show-error --retry 8 --retry-delay 5 -X POST \
"$NEXUS_URL?project=NEX&build=$BUILD_ID" -H "Authorization: Bearer $NEXUS_API_KEY" \
-H "Content-Type: application/xml" --data-binary @results.xml) \
&& ! printf '%s' "$out" | grep -q '"partial"' && break
sleep 5
done
Per-result detail is in Jira and in the ingestion log. The step runs even when tests failed (always(),
when: always, post { always }, succeededOrFailed(), Bitbucket's after-script),
because failed results are the ones you most want recorded.
Optional query parameters: testSet=<KEY> (record into that cycle), configuration=<id>,
environment=<label> (an alias such as stg resolves through the project's environment
catalogue), build=, commit=, component=, format= (junit-xml, json,
cucumber-json, xunit-xml, mocha-json, playwright-json, robot-xml, nunit-xml,
testng-xml, mochawesome-json, newman-json; detected from the body
when omitted), dataSet=<id> for data-driven results named ... [row-key], and
systemOut=comment (JUnit XML only, off by default) to post each test's own
<system-out> as a plain-text comment on its run — at most 50 comments per upload, each
clipped to 8,000 characters; the text never mentions or notifies anyone. Add
stepMatching=off to record only each test case's overall result: the run gets one row
with the pass or fail, and the case's steps are left untouched. By default Nexus matches a
result to the case's steps (a pass passes them all; a failure fails the first).
Replay protection (optional, from the next Nexus release). Two extra headers let
Nexus refuse a request that is sent again by someone or something else (a proxy, a copy of
a logged request): X-Nexus-Timestamp (the current Unix time in seconds) and
X-Nexus-Nonce (a random value, new for every send, including a resend). Nexus refuses a
nonce it has already seen from the same key, and a timestamp more than 5 minutes from its
own clock (400; the delivery log says which). The packaged integrations send both. With
curl, add them per send; because --retry would resend the same nonce, loop instead:
for i in 1 2 3 4 5 6 7 8; do
out=$(curl --fail --silent --show-error -X POST "$NEXUS_URL?project=NEX&build=$BUILD_ID" -H "Authorization: Bearer $NEXUS_API_KEY" -H "X-Nexus-Timestamp: $(date +%s)" -H "X-Nexus-Nonce: $(openssl rand -hex 16)" -H "Content-Type: application/xml" --data-binary @results.xml) && ! printf '%s' "$out" | grep -q '"partial"' && break
sleep 5
done
Without the headers everything works as before. The CI operations endpoint takes the same two headers; a batch sent without a nonce that is identical to one the same key sent under a minute ago is not run twice.
Replace NEX with your project key in every snippet.
GitHub Actions with curl
- name: Run tests
run: npx playwright test --reporter=junit
env:
PLAYWRIGHT_JUNIT_OUTPUT_NAME: results.xml
- name: Send results to Nexus
if: ${{ always() }}
env:
NEXUS_URL: ${{ secrets.NEXUS_URL }}
NEXUS_API_KEY: ${{ secrets.NEXUS_API_KEY }}
run: |
curl --fail --silent --show-error --retry 8 --retry-delay 5 -X POST \
"$NEXUS_URL?project=NEX&environment=staging&build=${GITHUB_RUN_NUMBER}&commit=${GITHUB_SHA}" \
-H "Authorization: Bearer $NEXUS_API_KEY" \
-H "Content-Type: application/xml" \
--data-binary @results.xml
GitLab CI with curl
test:
stage: test
script:
- npx playwright test --reporter=junit
variables:
PLAYWRIGHT_JUNIT_OUTPUT_NAME: results.xml
artifacts:
when: always
reports:
junit: results.xml
nexus-results:
stage: .post
when: always
needs:
- job: test
artifacts: true
script:
- >
curl --fail --silent --show-error --retry 8 --retry-delay 5 -X POST
"$NEXUS_URL?project=NEX&environment=staging&build=$CI_PIPELINE_IID&commit=$CI_COMMIT_SHA"
-H "Authorization: Bearer $NEXUS_API_KEY"
-H "Content-Type: application/xml"
--data-binary @results.xml
Define NEXUS_URL and NEXUS_API_KEY under Settings → CI/CD → Variables, with
Masked on for the key.
Jenkins with curl (declarative pipeline)
pipeline {
agent any
stages {
stage('Test') {
steps {
sh 'mvn -B test' // writes target/surefire-reports/*.xml
}
}
}
post {
always {
withCredentials([
string(credentialsId: 'nexus-url', variable: 'NEXUS_URL'),
string(credentialsId: 'nexus-api-key', variable: 'NEXUS_API_KEY')
]) {
sh '''
for f in target/surefire-reports/TEST-*.xml; do
curl --fail --silent --show-error --retry 8 --retry-delay 5 -X POST \
"$NEXUS_URL?project=NEX&environment=staging&build=$BUILD_NUMBER" \
-H "Authorization: Bearer $NEXUS_API_KEY" \
-H "Content-Type: application/xml" \
--data-binary @"$f"
done
'''
}
}
}
}
The single-quoted sh '''...''' block keeps Groovy from interpolating the secret; the
shell reads it from the environment. Surefire writes one file per test class, so the
loop posts each (every file must stay under 5 MB).
Jenkins with the nexus-upload CLI. There is no Nexus plugin for Jenkins, and none is
planned: the nexus-upload CLI is the Jenkins integration. It needs only Node.js 16+ on
the agent (the NodeJS plugin's nodejs(...) wrapper, a node Docker agent, or Node on
the path), detects the file's format and fills the build from BUILD_NUMBER and the
commit from GIT_COMMIT. It also resends
a large upload until Nexus has recorded all of it.
pipeline {
agent any
stages {
stage('Test') {
steps {
sh 'mvn -B test' // writes target/surefire-reports/*.xml
}
}
}
post {
always {
withCredentials([
string(credentialsId: 'nexus-url', variable: 'NEXUS_URL'),
string(credentialsId: 'nexus-api-key', variable: 'NEXUS_API_KEY')
]) {
sh '''
curl -fsSLo nexus-upload.mjs https://nexus.resync.nz/downloads/nexus-upload-0.3.8.mjs
for f in target/surefire-reports/TEST-*.xml; do
node nexus-upload.mjs "$f" --project NEX --environment staging
done
'''
}
}
}
}
The key reaches the CLI only through NEXUS_API_KEY (the CLI refuses an --api-key
flag, so it never lands in the build log or a process listing), and the CLI never prints
it; Jenkins also masks withCredentials values. Exit codes: 0 sent, 1 Nexus refused
the file or could not be reached, 2 a usage or file problem. Add --soft to warn instead
of failing the build.
Azure Pipelines with curl
steps:
- script: npx playwright test --reporter=junit
displayName: Run tests
env:
PLAYWRIGHT_JUNIT_OUTPUT_NAME: results.xml
- script: |
curl --fail --silent --show-error --retry 8 --retry-delay 5 -X POST \
"$(NEXUS_URL)?project=NEX&environment=staging&build=$(Build.BuildNumber)&commit=$(Build.SourceVersion)" \
-H "Authorization: Bearer $NEXUS_API_KEY" \
-H "Content-Type: application/xml" \
--data-binary @results.xml
displayName: Send results to Nexus
condition: succeededOrFailed()
env:
NEXUS_API_KEY: $(NEXUS_API_KEY)
Mark NEXUS_API_KEY as secret in the pipeline's variables; Azure only exposes a secret
to a script through an explicit env: mapping like the one above. An Azure Pipelines task
(NexusUpload@0) that does this step for you is described under "Packaged integrations".
Bitbucket Pipelines with curl
pipelines:
default:
- step:
name: Run tests
script:
- export PLAYWRIGHT_JUNIT_OUTPUT_NAME=results.xml
- npx playwright test --reporter=junit
after-script:
- >
curl --fail --silent --show-error --retry 8 --retry-delay 5 -X POST "$NEXUS_URL?project=NEX&environment=staging&build=$BITBUCKET_BUILD_NUMBER&commit=$BITBUCKET_COMMIT" -H "Authorization: Bearer $NEXUS_API_KEY" -H "Content-Type: application/xml" --data-binary @results.xml
Define NEXUS_URL and NEXUS_API_KEY under Repository settings → Repository
variables, with Secured on for the key. after-script runs whether the tests
passed or failed, so failed results are still sent.
Packaged integrations
If you would rather not write the curl step, use one of these. Download the available
ones, with install steps and checksums, from the
Integrations page. Each reads the same
settings from environment variables: NEXUS_URL, NEXUS_API_KEY (a key with the
ingest scope), NEXUS_PROJECT, and optionally NEXUS_ENVIRONMENT, NEXUS_TEST_SET,
NEXUS_BUILD, NEXUS_COMMIT and NEXUS_COMPONENT. Build and commit are filled from your
CI system when not set, and so is the CI job's page (NEXUS_BUILD_URL overrides it; https
only). None of them prints the key.
Current versions (12/10/2026): CLI 0.3.8, Playwright reporter 0.2.6, pytest plugin 0.2.5,
GitHub Action 0.3.6, Cypress plugin 0.1.5, Azure DevOps task 0.1.4, runner 0.6.9. The
runner, the Playwright reporter and the pytest plugin send an Idempotency-Key on every
upload (the same on each resend of it), and the pytest plugin now sends the CI job's page
too (--nexus-build-url / NEXUS_BUILD_URL). These send
XML reports in the JSON envelope, so stack traces and <system-out> comments keep their
line breaks, and the Playwright reporter and Cypress plugin now send each failure's stack
trace, not only its message. Against an older Nexus they fall back to sending the file as it
is (results still recorded once; traces lose their line breaks until Nexus is updated).
HTTPS only (28/09/2026: CLI 0.3.1, Playwright reporter 0.2.1, pytest plugin 0.2.1,
GitHub Action 0.3.1, Cypress plugin 0.1.1, Azure DevOps task 0.1.1). NEXUS_URL must
start with https://. A plain http:// URL is refused before the key or any results
leave your machine, and a redirect answer is never followed (the key would go with it):
copy the Results URL again from Project Settings → CI if you see either message. For
local testing against a stand-in on your own machine only, NEXUS_ALLOW_INSECURE_LOCALHOST=1
allows http://localhost, http://127.0.0.1 and http://[::1] — never any other host.
Large suites. Every Nexus upload tool sends a run of more than 500 results in parts of up to 500, one after another, into the same cycle and build, so you do not split anything yourself. A Postman/Newman report over 500 requests is the one exception: the tool stops with a clear message before sending. The tools also wait and resend when Nexus asks them to slow down (a rate limit), up to a cap.
Checking a download. Each file is listed in SHA256SUMS.txt; compare with
sha256sum <file> (or Get-FileHash <file> in PowerShell) before you use it.
| Integration | Package | Use it when |
|---|---|---|
| Playwright reporter | npm install --save-dev @resyncnz/nexus-playwright-reporter, or download nexus-playwright-reporter-0.2.6.tgz |
Playwright Test: sends the run at the end, no extra step |
| pytest plugin | pip install pytest-nexus, or download pytest_nexus-0.2.5-py3-none-any.whl |
pytest, including Selenium suites: sends the JUnit XML at the end |
| GitHub Action | nexus-github-action-0.3.6.zip (vendor into .github/actions/nexus-upload) |
GitHub Actions, any results file |
nexus-upload CLI |
npx @resyncnz/nexus-upload, or download nexus-upload-0.3.8.mjs (one file, no dependencies) |
Any other CI (including Jenkins), any results file; detects the format |
| Cypress plugin | npm install --save-dev @resyncnz/nexus-cypress, or download nexus-cypress-0.1.5.tgz |
Cypress: sends each cypress run at the end, no extra step |
| Azure Pipelines task | nexus-azure-devops-task-0.1.4.zip (upload to your organisation with tfx build tasks upload; not on the Visual Studio Marketplace) |
Azure Pipelines, any results files matched by a pattern |
Playwright reporter: install it from npm (or from the download above), then add it to the config.
npm install --save-dev @resyncnz/nexus-playwright-reporter
# or: npm install -D https://nexus.resync.nz/downloads/nexus-playwright-reporter-0.2.6.tgz
// playwright.config.ts
reporter: [['list'], ['@resyncnz/nexus-playwright-reporter', { project: 'NEX', environment: 'staging' }]],
pytest plugin: pip install pytest-nexus (or pip install https://nexus.resync.nz/downloads/pytest_nexus-0.2.5-py3-none-any.whl), then pytest --nexus-project NEX --nexus-environment staging. Name tests with the key using _, e.g.
test_NEX_11_valid_user_signs_in.
GitHub Action:
- name: Send results to Nexus
if: ${{ always() }}
uses: ./.github/actions/nexus-upload
with:
url: ${{ secrets.NEXUS_URL }}
api-key: ${{ secrets.NEXUS_API_KEY }}
project: NEX
file: results.xml
environment: staging
Cypress plugin (npm install --save-dev @resyncnz/nexus-cypress, or download it from the Integrations page;
or send Cypress's JUnit or mochawesome report with the CLI). It hooks after:run, so it sends once, when
cypress run ends:
// cypress.config.js
const { defineConfig } = require('cypress');
const nexus = require('@resyncnz/nexus-cypress');
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
nexus(on, config, { project: 'NEX', environment: 'staging' });
return config;
},
},
});
Put the key in the test title or an enclosing describe title
(it('NEX-12: a valid user signs in', ...)). Passed tests are recorded as Pass, failed ones
as Fail with the error message, and pending or skipped ones as Not Applicable; each test's
duration is sent. Screenshots and videos are not sent (Nexus takes results only). It also
reads NEXUS_CONFIGURATION and NEXUS_STEP_MATCHING (off records only each case's
overall result). Cypress keeps only the last after:run handler, so if another plugin
needs it too, call sendResults(results, options) from your own handler.
Azure Pipelines task (download it from the Integrations page; it is not on the Visual Studio Marketplace, so you can also use the CLI step under "The CLI in other CI systems"). It wraps the CLI, sends every file that matches the patterns, and masks the key:
- task: NexusUpload@0
displayName: Send results to Nexus
condition: succeededOrFailed()
inputs:
resultsFiles: '**/TEST-*.xml'
nexusUrl: $(NEXUS_URL) # secret variable
apiKey: $(NEXUS_API_KEY) # secret variable, never typed in
project: NEX
environment: staging
# format: junit-xml # default: detect from each file
# stepMatching: off # overall result only
# failOnError: false # warn instead of failing the task
A large upload that reaches Nexus's time limit is sent again automatically until Nexus has recorded all of it (CLI and GitHub Action 0.2.0, Playwright reporter and pytest plugin 0.2.0, runner 0.5.0); a resend never records a result twice.
By default the reporters and plugins only warn when an upload fails, so a Nexus problem
never fails your test run; turn on failOnUploadError / --nexus-fail-on-upload-error if
it should. The action, the Azure Pipelines task and the CLI fail the step, unless you set
fail-on-error: false / failOnError: false or pass --soft.
The CLI in other CI systems
nexus-upload <file> exits 0 when Nexus accepts the file, 1 when Nexus refuses it or
cannot be reached, and 2 for a usage or file problem.
Bitbucket Pipelines:
pipelines:
default:
- step:
name: Run tests
image: mcr.microsoft.com/playwright:v1.63.0-noble
script:
- npm ci
- PLAYWRIGHT_JUNIT_OUTPUT_NAME=results.xml npx playwright test --reporter=junit
after-script:
- curl -fsSLo nexus-upload.mjs https://nexus.resync.nz/downloads/nexus-upload-0.3.8.mjs && node nexus-upload.mjs results.xml --project NEX --environment staging
GitLab CI:
nexus-results:
stage: .post
image: node:22-alpine
when: always
needs:
- job: test
artifacts: true
script:
- curl -fsSLo nexus-upload.mjs https://nexus.resync.nz/downloads/nexus-upload-0.3.8.mjs && node nexus-upload.mjs results.xml --project NEX --environment staging
Azure Pipelines:
- script: curl -fsSLo nexus-upload.mjs https://nexus.resync.nz/downloads/nexus-upload-0.3.8.mjs && node nexus-upload.mjs results.xml --project NEX --environment staging
displayName: Send results to Nexus
condition: succeededOrFailed()
env:
NEXUS_URL: $(NEXUS_URL)
NEXUS_API_KEY: $(NEXUS_API_KEY)
Newman (Postman) needs no reporter of its own. Since 27/09/2026 Nexus reads Newman's own
JSON report: put the key in each request's name (NEX-12 Create user), run
newman run collection.json -r cli,json --reporter-json-export newman.json, then send
newman.json with the CLI (detected, or --format newman-json); one result per request.
The JUnit route still works when you want one result per pm.test instead: start each
test name with the key (pm.test('NEX-12: ...')), run newman run collection.json -r cli,junit --reporter-junit-export results.xml and send results.xml. The CLI also runs on
the Node 16 in the postman/newman image.
Other result formats
Change the body and Content-Type; everything else stays the same:
| Tool | Report | Body | Content-Type |
|---|---|---|---|
JUnit, PyTest (--junitxml), NUnit, TestNG, Robot Framework (--xunit), Playwright junit |
JUnit XML | the .xml file |
application/xml |
Cucumber (--format json) |
Cucumber JSON | the .json file |
application/json |
xUnit.net v2 (--xml) |
xUnit XML | the .xml file |
application/xml |
Mocha (--reporter json) |
Mocha JSON | the .json file |
application/json |
Playwright (--reporter=json) |
Playwright JSON | the .json file |
application/json |
Robot Framework (robot --output output.xml) |
Robot output.xml (format=robot-xml) |
output.xml |
application/xml |
NUnit 3 (nunit3-console --result=TestResult.xml, or dotnet test --logger "nunit;LogFilePath=TestResult.xml") |
NUnit 3 XML (format=nunit-xml) |
TestResult.xml |
application/xml |
| TestNG (Maven Surefire, Gradle) | TestNG XML (format=testng-xml) |
testng-results.xml |
application/xml |
Mocha / Cypress with mochawesome (--reporter-options json=true) |
mochawesome JSON (format=mochawesome-json) |
mochawesome.json (merge several with mochawesome-merge) |
application/json |
Postman / Newman (newman run collection.json -r json --reporter-json-export newman.json) |
Newman JSON (format=newman-json) |
newman.json |
application/json |
| Anything else | Nexus JSON | {"results":[{"automationKey":"NEX-123","status":"passed","durationMs":1234}]} |
application/json |
Durations in the report (time=, duration, durationMs) are kept and shown as the
run's Duration in the test case's execution history. For Cucumber JSON the run's duration is
the sum of the scenario's step durations (Cucumber writes them in nanoseconds), since
28/09/2026.
Where the Automation Key goes in the five native formats (27/09/2026):
- Robot Framework — a tag:
[Tags] NEX-123or[Tags] test_key:NEX-123, or start the test name with the key. - NUnit 3 —
[Property("test_key", "NEX-123")]or[Category("NEX-123")], or the key in the method name (NEX_123_Loginworks). - TestNG —
@Test(description = "NEX-123 Login"), a custom attribute@Test(attributes = @CustomAttribute(name = "test_key", values = "NEX-123")), or the key in the method name (NEX_123_login). Config methods and earlier retries are ignored. - mochawesome — the key in the
it()title, as for Mocha:it('NEX-123: logs in', …). - Postman / Newman — the key in the request's name, anywhere after a space or at the
start:
NEX-123 Create user. One result per request: a data-driven run (-d data.csv) folds its iterations into one result, failed if any iteration failed. A failed assertion, a script error or a request that could not be sent is a failure; a request whose tests were all skipped is Blocked.
# Robot Framework
robot --output output.xml tests/ || true
curl -sS -X POST "$NEXUS_URL?project=NEX&format=robot-xml" \
-H "Authorization: Bearer $NEXUS_API_KEY" -H "Content-Type: application/xml" \
--data-binary @output.xml
# TestNG (Maven)
mvn -B test || true
curl -sS -X POST "$NEXUS_URL?project=NEX&format=testng-xml" \
-H "Authorization: Bearer $NEXUS_API_KEY" -H "Content-Type: application/xml" \
--data-binary @target/surefire-reports/testng-results.xml
# Postman / Newman
newman run collection.json -e staging.postman_environment.json \
-r cli,json --reporter-json-export newman.json || true
curl -sS -X POST "$NEXUS_URL?project=NEX&format=newman-json" \
-H "Authorization: Bearer $NEXUS_API_KEY" -H "Content-Type: application/json" \
--data-binary @newman.json
# JUnit with each test's <system-out> kept as a comment on its run
curl -sS -X POST "$NEXUS_URL?project=NEX&systemOut=comment" \
-H "Authorization: Bearer $NEXUS_API_KEY" -H "Content-Type: application/xml" \
--data-binary @results.xml
# Overall result per test case only, steps left untouched
curl -sS -X POST "$NEXUS_URL?project=NEX&stepMatching=off" \
-H "Authorization: Bearer $NEXUS_API_KEY" -H "Content-Type: application/xml" \
--data-binary @results.xml
The CLI (nexus-upload --system-out comment), the GitHub Action (system-out: comment)
and the runner ("systemOut": "comment" on a tool) send the same option. For step
matching: nexus-upload --step-matching off (CLI 0.3.0, or NEXUS_STEP_MATCHING=off),
step-matching: off (Action 0.3.0) and "stepMatching": "off" on a runner tool or the
whole runner config (runner 0.6.0). A tool with Match results to steps turned off in
Project Settings → Automation records overall results for its runner jobs whatever the
runner sends.
Katalon Studio
Katalon writes a JUnit report for every Test Suite run, so no plugin is needed: send that file like any JUnit XML.
- Where the report is. Katalon Studio and Katalon Runtime Engine (
katalonc) writeReports/<run time>/<suite name>/<run time>/JUnit_Report.xmlinside the project. With-reportFolder=<folder>the files go under that folder instead. The easiest way to find it in a pipeline is to search for the file name, as below. - Automation Key. Put the key at the start of the Test Case's name in Katalon's Test
Explorer:
NEX-12 Login with valid credentials(orNEX_12 …). Katalon names each result by its repository path,Test Cases/Login/NEX-12 Login with valid credentials, and Nexus finds the key after the folder separator. Keep key-shaped words (REL-2) out of folder names, or the first one is taken as the key. - Results. Katalon's
PASSEDis Pass,FAILEDandERRORare Fail (the Katalon reason, for exampleStepFailedException: …, is the run's actual result and the Groovy stack trace its stack trace),SKIPPEDis Not Applicable. Each test case'stimebecomes the run's duration. A Test Suite Collection writes one report per suite: send each one.
# Katalon Runtime Engine (a Katalon licence / API key is needed to run it)
katalonc -noSplash -runMode=console -projectPath="$PWD/Shop.prj" \
-testSuitePath="Test Suites/Shop regression" -browserType="Chrome (headless)" \
-reportFolder=katalon-reports -apiKey="$KATALON_API_KEY" || true
report=$(find katalon-reports -name JUnit_Report.xml | head -n 1)
# With the nexus-upload CLI (keeps stack-trace line breaks)
node nexus-upload.mjs "$report" --project NEX --environment staging
# Or with curl, in the JSON envelope so the trace keeps its line breaks
printf '{"format":"junit-xml","encoding":"base64","content":"%s"}' "$(base64 < "$report" | tr -d '\r\n')" > envelope.json
curl --fail --silent --show-error -X POST "$NEXUS_URL?project=NEX&environment=staging" \
-H "Authorization: Bearer $NEXUS_API_KEY" -H "Content-Type: application/json" --data-binary @envelope.json
To keep each test case's Katalon log as a comment on its run, add --system-out comment
(CLI) or &systemOut=comment (curl).
Recording a deployment from CI
After a deploy job, record it so Nexus can tell current evidence from stale evidence. This uses the CI API URL (also shown on the API keys page) with the same key. The CI API can also create or update test cases, add them to a cycle, record a single result and record an environment event, in batches of up to 25 operations:
curl --fail --silent --show-error -X POST "$NEXUS_CI_API_URL?project=NEX" \
-H "Authorization: Bearer $NEXUS_API_KEY" \
-H "Content-Type: application/json" \
--data '{"operations":[{"op":"record-deployment","environment":"staging","version":"2.5.0","status":"success","externalId":"deploy-'"$BUILD_NUMBER"'","build":"'"$BUILD_NUMBER"'"}]}'
The response only says whether the batch was accepted. To see what each operation did
(its outcome, failure code, your externalKey and the Test Case it created or updated),
open Project Settings → CI → CI API log. It keeps the last 100 calls, newest first,
and can show failures only.
Reading test cases, steps, coverage and results
Use the separate read-only API to retrieve project test data as paged JSON. A project administrator enables Read test data on an API key and copies the Read API URL from the API keys screen. This permission is off by default and is never added to existing keys. The Results URL and write-oriented CI API remain separate endpoints.
Store the read URL and API key in your secret store as NEXUS_READ_URL and
NEXUS_API_KEY. Never publish their values or put the key in a URL.
curl --fail --silent --show-error \
"$NEXUS_READ_URL?project=ND&op=testCases&limit=50" \
-H "Authorization: Bearer $NEXUS_API_KEY"
| Operation | Returns |
|---|---|
testCases |
Test cases for the project |
testCase |
One test case, selected with key |
cycles |
Project cycles |
cycle |
One cycle, selected with key |
runs |
Runs; use cycle to filter to a cycle |
runSteps |
Step results, selected with run |
coverage |
Requirement coverage |
Set limit from 1 to 100 and pass the returned nextCursor as cursor to read
another page. The key reads only its own project. Records with Jira issue security
are never returned, sensitive test-data values are always masked, and people appear
only as Atlassian account IDs. Each key is limited to 60 requests a minute and
2,000 an hour.
For the full request and response contract, use the API reference and Read API OpenAPI JSON.
Jira search functions
Nexus search functions also work in Jira JQL. Use executedBy, runsWithStatus
(including custom run statuses), testsInSet and requirementsWithoutTests
alongside the existing functions. For example:
issue in requirementsWithoutTests("ABC")
This finds requirements with no tests in project ABC.
More troubleshooting
401: wrong key, revoked key, or a key from another project. The ingestion log shows the rejected attempt with the key's id (never the key).400: the body is not a recognised report, orconfiguration=names no Test Configuration in this project. With the replay headers: the nonce was already used (send a new one on every attempt, including a retry), or the timestamp is more than 5 minutes out (check the build machine's clock).- Results arrive but are unmatched: the test name does not start with a Test Case's Automation Key.
413: split the report; each request is limited to 5 MB.- "must be https" or "Nexus answered with a redirect": the URL is not the
https://Results URL from Project Settings → CI (or has changed). The packaged integrations never send the key over plain http or follow a redirect. - "partial: x of y results recorded" in the delivery log (the request answered
202with"status":"partial"): the upload reached the time limit. Nexus keeps what it recorded and, when it says "recording the rest in the background", finishes the upload itself (a later "background pass" entry shows it). Sending the same file again (with the same options) is always safe: Nexus records only what is missing and never records a result twice. The packaged integrations do this for you; if one reports "not confirmed finished", check the delivery log before re-running the step. "Still being processed" means an earlier send or a background pass of the same file is running; wait a few seconds and send it again. "Background processing stopped" means the background passes made no progress; send the same file again to finish it. - "tried again on the next pass" / "could not be recorded after 3 attempts": a Jira or
storage error hit some results before anything was written for them. Nexus answers
partialand retries them on the next pass (up to 3); after that the delivery is accepted and the log names how many were not recorded — send the same file again later to retry them. - A stack trace or failure message shows as one long line: the XML was sent as a raw body.
The Forge web trigger platform strips raw line breaks from the body before Nexus sees it
(confirmed live on 14.5.0:
lf=0on arrival), so evencurl --data-binary @fileloses them. Send the file with thenexus-uploadCLI, the runner or a packaged reporter, or wrap it in the base64 upload envelope shown under "Keep stack traces readable" above. (curl -d @file/--data @filealso removes line breaks on your side before sending; always use--data-binary.) Nexus keeps every line break that reaches it (LF, CRLF, CDATA and alike).
Runner troubleshooting: a queued automation run is not picked up
When a tester presses Run automation, Nexus only queues the run. A Nexus runner
(node nexus-runner.mjs) on your own test machine or CI agent must be running to pick it
up — Nexus never starts it and never connects to it.
- "No runner for Playwright has checked in since 3:12 pm" on the cycle, the Run card or
the Automation queue: no runner for that tool has asked Nexus for work for more than 3
minutes. Start the runner (
node nexus-runner.mjs, withNEXUS_API_KEYset), or cancel the run. "…has ever checked in": no runner has ever announced that tool — check the tool names innexus-runner.config.jsonmatch the tool cards in Project Settings → Automation (letter case does not matter). - "Runner version too old — upgrade to 0.6.2" (28/09/2026, SEC27-C4): Nexus gives jobs
only to runner 0.6.2 or later, the first with every runner-side security fix. An older
runner still polls but its claims are refused (
403 runner-too-old; the runner printsclaim refused (HTTP 403: runner-too-old)), so its runs stay queued. Replacenexus-runner.mjswith the current file from the integration packages (checkSHA256SUMS.txt), keep the config and restart it. Jobs it had already started still finish.docs/api-contracts.md§1e "Minimum runner version". - Project Settings → Automation shows each tool card's runner as Online, Offline since …, Never connected or Runner version too old — upgrade to 0.6.2, and when its last job finished. Test runner (step 4) checks a runner end to end.
- A runner reports in on every poll and every heartbeat while it runs a job, so a busy
runner stays Online. Keep
maxIdleSecondsat 60 or less (the default): a runner that waits longer between polls can read as Offline in between. - A queued run waits for the tool's Give up with no runner time (default 30 minutes) while no runner for it is online, then it is marked "Not picked up: no runner online", its run goes back to Not Run and the runner log says so. Press Run automation again once a runner is up.
- The person who queued the run gets a Jira notification when no runner has checked in for 10 minutes (Project Settings → Notifications → Runner not picked up, on by default). With nobody looking at the cycle or queue, an hourly check still releases the run and sends the notification, so either can arrive up to an hour late.
- A tool tag missing next to the A: the run's results came in without
&component=(the runner sends it; a CI job posting results directly may not). Nexus then shows the case's tool (its own, else the project default); add&component=<tool name>to the results URL to record the tool on the run itself.