Nexus · test management for Jira
Menu
Get Nexus

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

  1. Open your Jira project, then Project settings → Nexus Project Settings → CI.
  2. 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.
  3. 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.
  4. 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.
  5. Note your Jira project key and the environment you want the results associated with. The examples use NEX and staging; 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.

See GitLab's after_script behaviour.

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.

See Jenkins credentials and pipeline guidance.

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.

See Azure's variable and secret settings.

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.

See Bitbucket's step and after-script options.

Verify the first delivery

  1. Run a small pipeline on a branch that can access the configured secrets.
  2. 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.
  3. Return to Project settings → Nexus Project Settings → CI and refresh the delivery log. Check the processed outcome: matched, unmatched or skipped results.
  4. Open the matched Test Case's execution history. Check its status, duration and target environment against your test output.
  5. 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.


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-123 or [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_Login works).
  • 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) write Reports/<run time>/<suite name>/<run time>/JUnit_Report.xml inside 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 (or NEX_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 PASSED is Pass, FAILED and ERROR are Fail (the Katalon reason, for example StepFailedException: …, is the run's actual result and the Groovy stack trace its stack trace), SKIPPED is Not Applicable. Each test case's time becomes 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, or configuration= 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 202 with "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 partial and 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=0 on arrival), so even curl --data-binary @file loses them. Send the file with the nexus-upload CLI, 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 @file also removes line breaks on your side before sending; always use --data-binary.) Nexus keeps every line break that reaches it (LF, CRLF, CDATA and &#10; 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, with NEXUS_API_KEY set), or cancel the run. "…has ever checked in": no runner has ever announced that tool — check the tool names in nexus-runner.config.json match 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 prints claim refused (HTTP 403: runner-too-old)), so its runs stay queued. Replace nexus-runner.mjs with the current file from the integration packages (check SHA256SUMS.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 maxIdleSeconds at 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.