npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@resyncnz/nexus-runner

v0.6.9

Published

Customer-hosted runner: polls Nexus for Jira for queued automated test runs, runs them on your machine or CI, and reports the results back.

Readme

Nexus automation runner (0.6.9)

nexus-runner.mjs is a small script that lets testers start your automated tests from a Nexus cycle. In the cycle, they pick Run ▾ → Run automation on a case, or Run automation (n) for the whole cycle.

Nexus doesn't run tests inside Jira, and it never connects to your machines. The runner runs where your tests already run (a laptop, a CI agent or a container). It asks Nexus for queued runs, runs your test command locally and posts the results file back. Nexus then marks each run Pass or Fail.

The runner sends only these kinds of data:

  • claim, heartbeat and complete calls, which carry the action, the runner name, the names of the tools it can run, the job id, the claim's attempt id, a random claim request id (0.6.2) and a fixed error code;
  • on every call and upload (0.6.2), two replay-protection headers: X-Nexus-Timestamp (this machine's time) and a new random X-Nexus-Nonce. Nexus refuses a request it has already seen or one more than 5 minutes off its clock — keep this machine's clock right (NTP). The runner never resends such a refusal and says so in its output;
  • on every call and upload (0.6.3), X-Nexus-Runner-Version with this runner's version, so Nexus can refuse work to a runner that lacks the security fixes (see "Minimum version" below);
  • on every results upload (0.6.5), an Idempotency-Key (nexus-runner- and a random UUID) made once per results file and repeated on every resend of it, so Nexus records the file once however many sends it takes;
  • your results files, the same files you would post from CI, with the optional environment label from your config;
  • only when someone presses Test runner in Nexus: the runner version, the Node version, the OS platform (linux, win32, darwin) and, per configured tool, whether its command was found on this machine.

It sends no source code, environment variables, paths or logs.

Runner automation is Beta. A project administrator turns it on in Project Settings → Modules (Runner automation). While it is off, the runner gets no jobs for that project (it keeps polling slowly and picks work up once it is turned on). Projects that already used the runner before 28/09/2026 keep it on. Posting results from CI does not depend on this switch.

Install

From npm (Node 18 or later; the runner has no dependencies):

npx @resyncnz/nexus-runner --test
npm install --save-dev @resyncnz/nexus-runner   # or add it to a project

Alternative: download nexus-runner.mjs from nexus.resync.nz/downloads and run it with node nexus-runner.mjs. The examples below use node nexus-runner.mjs; with the npm package, run npx nexus-runner instead.

Requirements

  • Node.js 18 or later. The script has no dependencies, so there is no npm install.
  • A Nexus API key: Project Settings → CI, step 1. It must be allowed to use Runner and Send results (both are ticked for a new key; a key made before 27/09/2026 can use everything).
  • Test Cases whose Automation Status is Automated and whose Automation Key appears in the test's name. For example, a Test Case with key TC-123 needs a Playwright test called TC-123: user can log in.

Setup

  1. In Jira, open Project Settings → Automation in the Nexus project page.

    • Step 1, Automation tools: add the tools your tests use (Playwright, Selenium, Cypress, JUnit, Cucumber, Mocha, xUnit or Other), with how many runs of each may go at once and how long a run may wait and take.
    • Open a tool's card, then its Runner setup (owner request 27/09/2026): set the runner's name and environment and press Copy config. The config has both Nexus addresses, the project key, the environment and a tools entry for that tool only, with its command, results file and format — so each tool can run on its own machine. To run several tools on one machine, tick them under Combine tools to get one config with just those.
    • Step 3, Runner setup, has what every runner shares: the runner URL and how to start the runner.
  2. Copy nexus-runner.mjs into your test repository or onto your CI agent.

  3. Save the copied config as nexus-runner.config.json next to it (or start from nexus-runner.config.example.json), and edit each tool's command and resultsFile to match your project:

    | Field | Meaning | |---|---| | nexusUrl | The results URL (Project Settings → CI, step 2). | | runnerUrl | The runner URL (Project Settings → Automation, step 2). | | project | The Jira project key, for example RNX. | | apiKey | Optional. Prefer the NEXUS_API_KEY environment variable. The key never reaches your test commands (0.6.1): NEXUS_API_KEY, and any variable whose value contains the key, are removed from their environment. | | secretEnv | Optional (0.6.1). A list of more environment variable names your test commands must not see, for example ["GITHUB_TOKEN", "CI_JOB_TOKEN"]. At most 50. | | runnerName | The name testers see in "Running on ". Defaults to the host name. | | pollSeconds | How often to ask for work. The default and the minimum are 15 (0.4.0); 15-30 is recommended. A smaller value is raised to 15 with a warning; a value above 3600 (one hour) is a config error (0.6.7). | | maxIdleSeconds | Optional. The slowest the runner polls while nothing is queued (see "Idle back-off"). Default 120 (0.6.2; was 60), from pollSeconds up to 300 (or up to pollSeconds when that is above 300). | | cwd | Optional working directory for the commands. | | environment | The environment the runs record, for example Staging (1–100 characters). Sent as &environment= with every results file. | | allowCommandOverride | Optional, true or false (default false). When true, a Test Case's own Run command override (its Automation tab in Nexus) replaces the tool's command for that case. Off by default because that text comes from Jira and runs in this machine's shell: anyone who can edit a Test Case could choose it. Turn it on only when you trust everyone who can edit Test Cases in the project. | | stepMatching | Optional, "on" (default) or "off" (0.6.0). "off" adds &stepMatching=off to every results file: Nexus records only each case's overall result, as one row, and leaves the case's steps untouched. A tool's own stepMatching wins. | | tools | One entry per test tool, at most 20. See the next table. |

    Each entry in tools has these fields:

    | Field | Meaning | |---|---| | name | The tool's name as it is in Nexus (case doesn't matter). The runner announces it when it asks for work, so it only gets runs of tools it has. Also sent as &component= with the results. | | command | The shell command to run. It may contain placeholders (see below). | | resultsFile | The file the command writes. The runner deletes any old copy before each run. Nexus fills it from the tool's Results file (Project Settings → Automation), else the tool's usual file. Put {jobId} in it (0.6.2) to give every job its own file, e.g. .nexus-runs/{jobId}/results.xml — required for parallel above 1. The runner creates the folder and tells the command the full path in NEXUS_RESULTS_FILE. | | format | One of junit-xml, xunit-xml, robot-xml, nunit-xml, testng-xml, json, cucumber-json, mocha-json, mochawesome-json, newman-json or playwright-json. | | env | Optional extra environment variables for the command. {jobId} in a value is filled in (0.6.2). | | cwd | Optional working directory, relative to the top-level cwd. {jobId} gives every job its own folder (0.6.2; created for you, removed when left empty). | | timeoutSeconds | Optional. Stops the command sooner than the tool's Max run time in Nexus (the runner always stops it at that). | | environment | Optional. Overrides the top-level environment for this tool's results. | | parallel | Optional, 1–20. At most this many runs of the tool at once on this runner. Nexus's Parallel runs for the tool is the project-wide limit; the runner uses the smaller of the two. 0.6.2: above 1 only with {jobId} in resultsFile (otherwise two jobs would write, delete and post the same file); a tool without it runs one job at a time. | | allowCommandOverride | Optional, true or false. Overrides the top-level allowCommandOverride for this tool. | | systemOut | Optional, "comment" only. JUnit results: Nexus posts each test's own <system-out> as a plain-text comment on its run (at most 50 per upload, 8,000 characters each). Off when absent. | | stepMatching | Optional, "on" or "off" (0.6.0). Overrides the top-level stepMatching for this tool's results. Nexus also records overall results only for a job whose tool has Step matching turned off in Project Settings → Automation, whatever this says. |

  4. Check the machine, without calling Nexus:

    node nexus-runner.mjs --test

    It reads the config and, for each tool, says whether its command (the first word, for example npx) is found on this machine, where the results file goes, and what a filled-in command looks like. It exits with 1 if anything is missing.

  5. Start the runner:

    export NEXUS_API_KEY=nxs_...        # PowerShell: $env:NEXUS_API_KEY = "nxs_..."
    node nexus-runner.mjs                 # uses ./nexus-runner.config.json
    node nexus-runner.mjs other.json      # or name a config file
    node nexus-runner.mjs --once          # poll once and exit (for a scheduled CI job)

    Leave it running. Press Ctrl+C to stop it once the current jobs finish.

  6. In Nexus, press Test runner (Project Settings → Automation, step 3). Within a few seconds it shows, for example: "Runner docker-lab-runner answered in 3.2 s · v0.3.1 · Playwright ✓ · Selenium ✓". A ✗ means that tool's command wasn't found on the runner's machine.

Placeholders

The runner replaces these placeholders in command with the Automation Keys of the runs in the job:

| Placeholder | Example | For | |---|---|---| | {grep} | (?:^\|[^A-Za-z0-9_])TC-11(?![A-Za-z0-9_])\|… (regex-escaped; each key as a whole word, so TC-1 never selects TC-10, 0.6.8) | npx playwright test --grep {grep}, mocha --grep {grep} | | {pytestK} | TC_11_ or TC_42_ | pytest -k {pytestK}. Pytest names can't contain -, so every character other than a letter, digit or _ becomes _. The trailing _ (0.6.8) keeps TC_1_ from selecting test_TC_10_…, so name tests test_TC_11_<title>. | | {keys} | TC-11,TC-42 | Your own scripts |

Write placeholders without quotes (0.4.1). The runner quotes every value itself for your shell — single quotes on Linux/macOS (--grep 'TC-11|TC-42'), double quotes on Windows cmd.exe — so a value always stays one argument. A config from before 0.4.1 with "{grep}" or '{grep}' still works: the runner replaces those quotes with its own.

Upgrading to 0.4.2: a placeholder can't sit inside a larger quoted string. A command such as --grep "@smoke|{grep}" or -k "{pytestK} and not slow" put the runner's quotes in the middle of your string under 0.4.1, so the wrong tests were selected without any error. 0.4.2 refuses it instead: --test and start-up report a config error naming the tool, and a Test Case command written that way is skipped with a note in the output. Put the environment variable inside your string instead — --grep "@smoke|$NEXUS_GREP" or -k "$NEXUS_PYTEST_K and not slow" on Linux/macOS, %NEXUS_GREP% / %NEXUS_PYTEST_K% in Windows cmd.exe — or keep the placeholder on its own outside any quotes. A bare {grep} and a self-quoted "{grep}" are unchanged.

Recommended: the environment variables. The same values are set as NEXUS_GREP, NEXUS_PYTEST_K and NEXUS_KEYS (plus NEXUS_JOB_ID, NEXUS_TEST_SET, NEXUS_RESULTS_FILE and, for a case group with its own environment, NEXUS_ENVIRONMENT). They never touch the command line, so where your tool or a wrapper script can read them, prefer them to placeholders — for example pytest -k "$NEXUS_PYTEST_K" in a script, or reading process.env.NEXUS_GREP in a Playwright config.

{jobId} (0.6.2). {jobId} in command, resultsFile, cwd or an env value is replaced with the job's id (always a UUID Nexus sent — anything else is refused), so a command can write to a per-job path: --junitxml=.nexus-runs/{jobId}/results.xml.

The rule, in short:

| Your tool's parallel | {jobId} in resultsFile | What happens | |---|---|---| | absent or 1 | not needed | One job of the tool at a time. The runner deletes the results file, runs the command, posts the file. A fixed path such as results.xml is fine. | | 1 | used | Same, but every job keeps its own file (handy when you want to look at an old run's report). | | above 1 | required | Jobs of the tool run side by side, each writing its own file. Without {jobId} the runner refuses the config (--test and start-up say so), because two jobs would write, delete and post the same file. |

When you use {jobId} in resultsFile, the command must write to the same path: put {jobId} in the command's own output option too (--junitxml=.nexus-runs/{jobId}/results.xml), or read NEXUS_RESULTS_FILE (the full path, set for every job), or set the tool's own variable in env as the example config does for Playwright ("PLAYWRIGHT_JUNIT_OUTPUT_NAME": ".nexus-runs/{jobId}/playwright-results.xml"). If the command writes somewhere else, the runner finds no file and Nexus shows "No result from the runner".

Windows cmd.exe and %VAR%. cmd.exe expands %VAR% before it reads the line, so only use variables whose values the runner controls: %NEXUS_GREP%, %NEXUS_KEYS%, %NEXUS_PYTEST_K%, %NEXUS_JOB_ID% and %NEXUS_RESULTS_FILE% (quote the last one; paths may hold spaces). Since 0.6.2 NEXUS_ENVIRONMENT is set only when the label is plain text (letters, digits, space and . _ : @ / ( ) + -) and NEXUS_TEST_SET only when it is an issue key; otherwise they are left out and the runner says so.

Automation Keys come from Jira and end up in a shell command. For that reason, since 0.4.1 the runner only uses keys made of letters, digits and _ . : @ / -, at most 200 characters, with no spaces and not starting with - (so a key can never add words or options to your command). It skips any other key and says so in its output. Since 28/09/2026 Nexus enforces the same rule itself: it refuses such a key when it is saved (in Nexus, in Jira's own issue screens, from CI or an import) or queued, and never sends one to any runner — older runners included.

Nexus matches results to Test Cases the same way as any other results upload. In a JUnit or xUnit file the key can lead the name (TC-123: …), sit inside it (test_login[TC-123]), use the code-safe form (test_TC_123_login, TC_123_signIn()), or come from a test_key/automationKey property or trait. For pytest -k "{pytestK}" to select a test, its name must contain the code-safe key, e.g. def test_TC_123_login():.

A Test Case's own command and environment (0.3.1)

A Test Case's Automation tab in Nexus can set its own Environment and Run command override. Since 27/09/2026 only a project admin can set them (Nexus keeps them in its own storage, not on the Jira issue). The claim lists them per case (cases[].environment, cases[].commandOverride, only when a project admin set them).

  • Environment. The case's results are sent with its own &environment=, instead of the tool's or the config's. Cases with the same environment still run in one command.
  • Run command override. Used only when allowCommandOverride is true (the config's, or the tool's own). The case then runs on its own with that command; the placeholders and NEXUS_* variables work in it the same way, and it must write the tool's resultsFile. With allowCommandOverride off, the override is ignored and counted in the runner's output ("ignored 1 Test Case command(s)"), and the case runs with the rest. Nexus and the runner both accept only one printable line of at most 500 characters.

NEXUS_ENVIRONMENT is set for a case group with its own environment. --test says, per tool, whether Test Case commands would run.

Idle back-off (0.4.0)

Every call the runner makes runs a Nexus function in your Atlassian site, so an idle runner slows down:

  • It asks every pollSeconds (15 by default).
  • After 4 empty answers in a row the wait doubles, and doubles again after 4 more, up to maxIdleSeconds: 15 s, then 30 s, then 60 s by default.
  • It goes back to pollSeconds as soon as a job arrives, and stays there while a job is running.
  • When Nexus answers "nothing queued" it may add retryAfterSeconds (30 today). The runner waits at least that long, still no longer than maxIdleSeconds.

So a run queued while the runner is idle starts within about a minute. Lower maxIdleSeconds (down to pollSeconds) if you need faster pick-up.

A results file that reaches Nexus's time limit part-way (a 202 whose body says "partial", or a 500), a 502/503/504 or a network error is posted again, up to 8 sends, until Nexus has recorded all of it; a resend never records a result twice (0.5.0, 27/09/2026).

What happens during a run

  1. A tester queues a run. Nexus creates one new run per case in the cycle, sets it to Not Run and shows it as "Queued — waiting for a runner". The cases are grouped by their automation tool (the case's own, else the project default) and each tool gets its own job. A case whose Test Case is Obsolete is left out.
  2. The runner asks for work, naming the tools it has a free slot for. Nexus hands it the oldest queued job of one of those tools, as long as that tool has fewer running jobs than its Parallel runs. Nexus shows "Running on " and moves the job's runs to In Progress. A run a tester already started or finished is left alone.
  3. The runner runs that tool's command (one command per group of cases that share a run command and environment — usually one for the whole job), sending a heartbeat every 60 seconds, and posts resultsFile to nexusUrl?project=…&format=…&job=<jobId>&testSet=<cycle>&component=<tool>&environment=<environment>. Results for the job's cases fill exactly the runs Nexus created, which then become Pass or Fail. Results for other keys are handled as a normal upload.
  4. The runner reports that it finished. If the tool produced no results, it reports no-results. If the command couldn't start, it reports command-failed.
  5. Any run in the job that got no result goes back to Not Run, and its row in the cycle says why: "No result from the runner", "Runner failed: …", "Runner didn't pick this up" or "Automation run cancelled". The note goes away when the run gets a result. If the project's Test Execution workflow can't move the run back (it was set up before Nexus added that step, or the cycle was closed meanwhile), the run stays In Progress with the same note; a Jira administrator adds the missing step from Nexus Setup, or a project administrator with Auto fix in a team-managed project.

The runner can run several jobs at once (at most one command per job), up to each tool's limit. Two runs that would write the same results file never overlap: the second waits until the first has posted its file (0.6.2). Jobs of the same tool are taken oldest first. A job is only ever given to one runner, even when several ask at the same moment, and a tool's Parallel runs holds across all your runners together.

When things go wrong (0.6.1, 0.6.2).

  • If Nexus stops holding the job for this runner — it timed out while the network was down, it was already finished, or the API key was revoked — the next heartbeat says so. The runner stops the command, logs why, and posts nothing for that job.
  • If the runner cannot tell Nexus the job finished (network error, Nexus busy), it tries again a few times, waiting longer each time. Nexus answers a repeated report the same way, so nothing is counted twice. Since 0.6.2 a report that still could not be sent is kept in .nexus-runner-state.json next to the config and sent again on the next polls — also after a restart — for up to a day.
  • If the answer to a claim is lost (0.6.2), the runner asks again with the same claim request id and Nexus gives it the job it had already handed out, so no job is left held by nobody. Every call to Nexus gives up after 70 seconds, and heartbeats never overlap.
  • The runner remembers the test processes it starts in .nexus-runner-children.json next to the config. If the runner itself is killed mid-run (kill -9, a crash), the next runner started with the same config stops what was left running (the whole process group, or taskkill /T on Windows) before it asks for work. It stops a process only when it is sure it is one a runner started: on Linux its environment holds the runner's marker; elsewhere (0.6.2) its start time must match the one recorded, so a process that got the same PID after a reboot is left alone. Entries older than a day are ignored.
  • One runner per config (0.6.2). The runner takes .nexus-runner.lock next to its config and refuses to start while another live runner holds it — give each runner its own config file. A lock left by a runner that is gone is taken over. Since 0.6.8 the lock, children and state files belong to the config, not the folder: nexus-runner.config.json keeps the plain names, any other config gets its own (nexus-runner.B.json uses .nexus-runner.nexus-runner.B.lock …), so two configs in one folder run two runners.
  • Run time (0.6.2). The tool's Max run time bounds the whole job, not each command of it.
  • A results file older than the command that should have written it is never posted (it is left over from an earlier run).

Cancel. Anyone can cancel a run they queued, and a project administrator any run, from the Automation queue (Project Settings → Automation, step 5, or the cycle toolbar). A waiting job is simply dropped. For a running job, the runner learns on its next heartbeat and stops the command (and anything it started).

Deadlines (per tool, set in Nexus):

  • If no runner takes a queued job within the tool's Max queue time (default 60 minutes), the job shows "Runner didn't pick this up (expired)".
  • The runner stops a command at the tool's Max run time (default 60 minutes).
  • Nexus gives up on a running job when the runner has gone quiet: no heartbeat for 5 minutes once the job is past its Max run time, or no heartbeat for 15 minutes at all. It then shows "Runner failed: it didn't finish in time". If its results had already arrived by then, Nexus counts the job as done. (A runner older than 0.3.0 sends no heartbeats; its jobs time out at the tool's Max run time.)

Docker

FROM mcr.microsoft.com/playwright:v1.47.0-jammy
WORKDIR /tests
COPY . .
RUN npm ci
CMD ["node", "nexus-runner.mjs"]
docker build -t my-tests-runner .
docker run -d --restart unless-stopped \
  -e NEXUS_API_KEY=nxs_... \
  -v "$PWD/nexus-runner.config.json:/tests/nexus-runner.config.json:ro" \
  my-tests-runner

Any image with Node 18+ and your test tools works. The container needs outbound HTTPS to your Atlassian site's web trigger host and nothing inbound.

Run the runner in Docker (tests in their own containers)

If your tests already run as Docker Compose services (one container per tool), run the runner as one more Compose service and let it start the test containers next to it. Its output, and each test container's output, then show up in Docker Desktop and in docker compose logs.

  1. An image with Node and the Docker CLI. The runner starts the test containers through the host's Docker daemon, so the image needs the docker CLI and the Compose plugin, not the test tools:

    FROM docker:cli AS dockercli
    FROM node:24-bookworm-slim
    COPY --from=dockercli /usr/local/bin/docker /usr/local/bin/docker
    COPY --from=dockercli /usr/local/libexec/docker/cli-plugins/docker-compose /usr/local/libexec/docker/cli-plugins/docker-compose
    WORKDIR /lab

    Add a .dockerignore containing *: the image needs nothing from your folder, and your .env stays out of the build.

  2. The service. Mount your project folder (config, runner script, test scripts) and the Docker socket, and pass the API key with env_file only — never bake it into the image or write it in the Compose file:

    name: my-tests            # one Compose project name, inside and outside the container
    services:
      nexus-runner:
        build: { context: ., dockerfile: Dockerfile.runner }
        restart: unless-stopped
        init: true
        working_dir: /lab
        env_file: [.env]      # NEXUS_API_KEY=...
        environment:
          - HOST_DIR=/run/desktop/mnt/host/c/Users/me/my-tests   # this folder, as the daemon sees it
          - COMPOSE_PROJECT_NAME=my-tests
          - TINI_KILL_PROCESS_GROUP=1
        volumes:
          - ./:/lab
          - /var/run/docker.sock:/var/run/docker.sock
        command: ["bash", "-c", "node nexus-runner.mjs 2>&1 | tee -a runner.log"]

    docker compose up -d --build nexus-runner, then docker compose logs -f nexus-runner.

  3. Bind mounts resolve on the host. When the runner calls docker compose run … inside its container, the host daemon creates the test container, so a relative source like ./tests:/work would point at the runner's /lab, which the host does not have. Write the test services' bind sources as ${HOST_DIR:-.}/tests:/work: from the host HOST_DIR is unset and . applies; inside the runner it is the host path. On Docker Desktop for Windows use the /run/desktop/mnt/host/<drive>/<path> form (for example /run/desktop/mnt/host/d/work/my-tests) — a D:/… path is not an absolute path to the Linux Compose CLI inside the container. On Linux and macOS use the plain host path.

  4. Keep the test containers for their logs. docker compose run --rm deletes a container, and its output, when it ends. In your tool commands use docker compose run --no-deps --name <tool>-<time> <service> … instead, and remove all but the last few runs of each tool afterwards. Start the services the tests need (the app under test, Selenium) once, outside the run; --no-deps stops Compose from recreating them on each run. Results files still land in your folder through the bind mount, where the runner reads them.

Mounting the Docker socket gives the runner container full control of the host's Docker daemon. Use it only on a machine dedicated to your tests, and keep the runner's allowCommandOverride off unless you trust everyone who can edit Test Cases.

Minimum version (28/09/2026)

Nexus gives jobs only to runner 0.6.2 or later. Older runners (0.2.0–0.6.1) lack security fixes — Automation Keys quoted and checked before they reach your command line, one results file per job, the API key kept out of your test command's environment, https only, no redirects, replay headers — so their claims are answered 403 runner-too-old and they get no runs. A job an old runner already started still finishes (heartbeats and completes are never refused for version).

  • In the runner's output an old runner prints claim refused (HTTP 403: runner-too-old); 0.6.3 prints "Nexus refused this runner … as too old — upgrade to …" and waits 5 minutes before asking again.
  • In Nexus, Project Settings → Automation shows Runner version too old — upgrade to 0.6.2 on the tool, with the runner's name and version; the Automation queue and a queued run say the same, and the runner log shows Runner too old.
  • To upgrade, replace nexus-runner.mjs with the current file from https://nexus.resync.nz/downloads/ (check it against SHA256SUMS.txt there), keep your config, and restart the runner. No config change is needed unless a tool uses parallel above 1 (see "Upgrading to 0.6.2").

Upgrading to 0.6.9 (must be republished)

  • A 429 on a results upload is sent again after its Retry-After, like the CLI.
  • A config saved with a byte-order mark or as UTF-16 loads; an invalid config no longer quotes part of itself in the error (it may hold the API key).
  • --test and the Test runner report check the program a command really runs (cd x && npx ..., set CI=1&& npm test, NAME=v cmd).
  • A stopped command's process group is killed after the grace period even if its shell already exited.
  • timeoutSeconds must be a number above 0 (and at most 604800).
  • Ctrl+C or SIGTERM while the runner is idle ends the wait at once instead of after the idle sleep (up to 5 minutes).

Upgrading to 0.6.8

  • {grep} matches each Automation Key as a whole word: a job for NEX-1 no longer also runs (and records) NEX-10 … NEX-199. {pytestK} ends each key with _ for the same reason — name pytest tests test_NEX_1_<title>.
  • A config not named nexus-runner.config.json has its own lock, children and state files, so a second config in the same folder starts a second runner. On first start a lone config of that kind takes over the plain-named state and children files its older runner left.

Upgrading to 0.6.7

  • A job that Nexus cancels while it waits for another job to finish with the same results file no longer starts its command afterwards, and never deletes or changes that file.
  • The time a job spends waiting for a shared results file now counts against its maxRunMinutes. If the time runs out while it waits, the job ends as timed out without running its command; otherwise the command gets only the time that is left.
  • pollSeconds above 3600 is refused as a config error (a huge value used to make the runner poll with no wait at all). No other config change.

Upgrading to 0.6.6

  • When Nexus is busy (Forge storage at its rate limit) it answers 503 with a Retry-After header. The runner now waits that long before its next claim, and before resending a complete or a results upload (up to 60 s per resend), instead of its own shorter pause. A busy answer is never treated as a refusal: nothing is lost, the job and its results are sent again. No config change.

Upgrading to 0.6.5

  • Each results upload carries an Idempotency-Key, the same on every resend of that file (a continuation after "partial", a retry after a network error or 5xx, and the raw-file fallback). A resend that arrives late is still answered from what Nexus recorded, never recorded twice. No config change.

Upgrading to 0.6.4

  • XML results (JUnit, xUnit, NUnit, TestNG, Robot) are posted inside a small JSON envelope (base64), so stack traces and <system-out> comments keep their line breaks — before, every line of a trace arrived run together. Against a Nexus older than 28/09/2026 the runner sends the file as it is instead (results recorded once, lines lost). No config change.

Upgrading to 0.6.3

  • Adds the X-Nexus-Runner-Version header and the plain "too old" message above. Nothing else changes; no config change.

Upgrading to 0.6.2

  • nexusUrl and runnerUrl must be https (they always were in the examples); for a local test against this machine only, NEXUS_ALLOW_INSECURE_LOCALHOST=1 allows http://localhost / 127.0.0.1 / [::1]. A redirect from Nexus is never followed — the key never goes to another address.

  • An idle runner now slows to every 2 minutes by default (maxIdleSeconds 120), and waits as long as Nexus asks (up to 5 minutes, e.g. while runner automation is off).

  • A tool with parallel above 1 needs {jobId} in its resultsFile (and the tool pointed at that path — see the example config); --test and start-up say so. Without it the tool runs one job at a time.

  • Run one runner per config file (the lock file enforces it).

  • Nothing else changes: 0.6.2 works with an older Nexus (it stops sending the claim request id when Nexus refuses it).

If the runner URL leaks

The runner URL alone is not enough to take work: every call needs a valid API key with the runner scope, and since 28/09/2026 Nexus checks the key before any failure budget, so a flood of bad requests cannot lock your runners out. To be safe after a leak, revoke and recreate the API key (Project Settings → CI). Nexus has no button yet to replace the runner URL itself (Forge allows it — a new URL, then the old one deleted — and it is on the backlog); until then, ask Resync support if the URL must change.

Troubleshooting

  • Test runner says "No runner answered": the runner isn't running, points at another project, or is older than 0.3.0. Run node nexus-runner.mjs --test on its machine.
  • claim refused (HTTP 401): the API key is wrong or revoked, project doesn't match the key's project, or the key isn't allowed to use the Runner (see its "Can use" column on the CI tab). The runner log (Project Settings → Automation) shows "Unauthorized"; failed calls are logged at most once a minute.
  • claim refused (HTTP 403: runner-too-old) (or "as too old" from 0.6.3): the runner is older than Nexus accepts — see "Minimum version".
  • claim refused (HTTP 429): too many calls in a minute, usually several runners on one project polling fast. Raise pollSeconds.
  • "Last runner seen" in Project Settings → Automation is updated at most every 5 minutes while nothing changes, and the runner log lists jobs, results, cancels and errors but not empty polls (0.4.0), so a quiet log with a recent "Last runner seen" means the runner is polling and nothing is queued for its tools.
  • Runs stay "Queued": no runner has that tool (check the tool name matches Nexus), the tool is at its Parallel runs limit, or no runner is running.
  • Runs pass in the runner but show "No result from the runner" in Nexus: the results don't name the Automation Key. The delivery log (Project Settings → CI, step 4) lists unmatched keys.
  • Runs show no environment: set environment in the config (or on the tool).
  • A stack trace or <system-out> comment arrives as one run-on line: the runner is older than 0.6.4. Upgrade it (see "Upgrading to 0.6.4").

Test output as run comments (systemOut)

Set "systemOut": "comment" on a JUnit tool to have Nexus post each test's own <system-out> as a plain-text comment on its run, headed "Automation output (system-out)". The cycle run panel shows the newest one under Test output, with a link to the rest in the run's Comments tab.

  • Only JUnit XML carries <system-out> per test; the setting does nothing for other formats.
  • Your tests must print something, and your JUnit writer must keep it: pytest needs -o junit_logging=system-out (or all) for captured output to reach the XML; Maven Surefire keeps it by default. Check your tool's JUnit writer if nothing appears.
  • At most 50 comments per upload and 8,000 characters each (a longer output is cut with a note). Nothing is posted for a test with empty output.
  • Comments are posted as the app, as plain text — never rendered as markup.
  • Off when absent. The same switch exists for CI uploads: --system-out comment on nexus-upload, system-out: comment on the GitHub Action.
{ "name": "JUnit", "command": "mvn -q test -Dtest={grep}", "resultsFile": "target/surefire-reports/TEST-results.xml", "format": "junit-xml", "systemOut": "comment" }

Tests

npm run test:runner

Licence

MIT. © 2026 Resync Consulting Limited. See LICENSE.