@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.
Maintainers
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 randomX-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-Versionwith 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
environmentlabel 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 projectAlternative: 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
Automatedand whose Automation Key appears in the test's name. For example, a Test Case with keyTC-123needs a Playwright test calledTC-123: user can log in.
Setup
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
toolsentry 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.
Copy
nexus-runner.mjsinto your test repository or onto your CI agent.Save the copied config as
nexus-runner.config.jsonnext to it (or start fromnexus-runner.config.example.json), and edit each tool'scommandandresultsFileto 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 exampleRNX. | |apiKey| Optional. Prefer theNEXUS_API_KEYenvironment 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), frompollSecondsup to 300 (or up topollSecondswhen that is above 300). | |cwd| Optional working directory for the commands. | |environment| The environment the runs record, for exampleStaging(1–100 characters). Sent as&environment=with every results file. | |allowCommandOverride| Optional,trueorfalse(defaultfalse). Whentrue, 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=offto every results file: Nexus records only each case's overall result, as one row, and leaves the case's steps untouched. A tool's ownstepMatchingwins. | |tools| One entry per test tool, at most 20. See the next table. |Each entry in
toolshas 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 forparallelabove 1. The runner creates the folder and tells the command the full path inNEXUS_RESULTS_FILE. | |format| One ofjunit-xml,xunit-xml,robot-xml,nunit-xml,testng-xml,json,cucumber-json,mocha-json,mochawesome-json,newman-jsonorplaywright-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-levelcwd.{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-levelenvironmentfor 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}inresultsFile(otherwise two jobs would write, delete and post the same file); a tool without it runs one job at a time. | |allowCommandOverride| Optional,trueorfalse. Overrides the top-levelallowCommandOverridefor 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-levelstepMatchingfor 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. |Check the machine, without calling Nexus:
node nexus-runner.mjs --testIt 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.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.
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
allowCommandOverrideistrue(the config's, or the tool's own). The case then runs on its own with that command; the placeholders andNEXUS_*variables work in it the same way, and it must write the tool'sresultsFile. WithallowCommandOverrideoff, 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
pollSecondsas 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 thanmaxIdleSeconds.
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
- 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.
- 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.
- 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
resultsFiletonexusUrl?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. - The runner reports that it finished. If the tool produced no results, it reports
no-results. If the command couldn't start, it reportscommand-failed. - 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.jsonnext 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.jsonnext 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, ortaskkill /Ton 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.locknext 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.jsonkeeps the plain names, any other config gets its own (nexus-runner.B.jsonuses.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-runnerAny 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.
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
dockerCLI 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 /labAdd a
.dockerignorecontaining*: the image needs nothing from your folder, and your.envstays out of the build.The service. Mount your project folder (config, runner script, test scripts) and the Docker socket, and pass the API key with
env_fileonly — 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, thendocker compose logs -f nexus-runner.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:/workwould 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 hostHOST_DIRis 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) — aD:/…path is not an absolute path to the Linux Compose CLI inside the container. On Linux and macOS use the plain host path.Keep the test containers for their logs.
docker compose run --rmdeletes a container, and its output, when it ends. In your tool commands usedocker 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-depsstops 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.mjswith the current file from https://nexus.resync.nz/downloads/ (check it againstSHA256SUMS.txtthere), keep your config, and restart the runner. No config change is needed unless a tool usesparallelabove 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).
--testand 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.
timeoutSecondsmust 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 forNEX-1no longer also runs (and records)NEX-10…NEX-199.{pytestK}ends each key with_for the same reason — name pytest teststest_NEX_1_<title>.- A config not named
nexus-runner.config.jsonhas 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. pollSecondsabove 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
503with aRetry-Afterheader. The runner now waits that long before its next claim, and before resending acompleteor 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-Versionheader and the plain "too old" message above. Nothing else changes; no config change.
Upgrading to 0.6.2
nexusUrlandrunnerUrlmust be https (they always were in the examples); for a local test against this machine only,NEXUS_ALLOW_INSECURE_LOCALHOST=1allowshttp://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 (
maxIdleSeconds120), and waits as long as Nexus asks (up to 5 minutes, e.g. while runner automation is off).A tool with
parallelabove 1 needs{jobId}in itsresultsFile(and the tool pointed at that path — see the example config);--testand 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 --teston its machine. claim refused (HTTP 401): the API key is wrong or revoked,projectdoesn'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. RaisepollSeconds.- "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
namematches 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
environmentin 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(orall) 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 commentonnexus-upload,system-out: commenton 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:runnerLicence
MIT. © 2026 Resync Consulting Limited. See LICENSE.
