Skip to content

Use in CI

This page wires a release job and a consumer job into GitLab CI.

Credentials

Set masked variables in your CI system and let nxr resolve them:

variables:
  NXR_USERNAME: ci-bot          # masked
  NXR_PASSWORD: $SECRET_TOKEN   # masked

or precompute the compact form once and store it as a single secret:

variables:
  NXR_AUTH: $BASE64_CREDENTIALS   # base64 of "user:pass"

printf 'ci-bot:%s' "$TOKEN" | base64 produces the NXR_AUTH value. Every command takes its URL from argv, so nothing persists between jobs.

Half-set credentials are a misuse error, caught before any request:

$ NXR_USERNAME=ci-bot nxr up dist/1.4.0/ https://nexus.example.com/repository/raw-main/1.4.0/
error: misuse: NXR_USERNAME and NXR_PASSWORD must be set together
hint: check the command line arguments

Avoid -u user:pass in scripts

-u is for interactive debugging. In a script it lands in the command line, which ps shows to every user on the machine.

Exit codes are the API

Exit 0 continues, 1 is a data problem (a retry fails identically), 2 is a broken job definition, 3 is transport trouble (the same command is expected to succeed on a later run). The cause-and-fix table: exit codes. The full taxonomy: errors and exit codes.

Transfers are resumable and re-runs are free, so a retry loop is always safe to write:

BASE=https://nexus.example.com/repository/raw-main
nxr up "dist/$VERSION/" "$BASE/$VERSION/" || status=$?
case ${status:-0} in
  0) echo published ;;
  3) echo "transport trouble, retrying later"; sleep 60
     nxr up "dist/$VERSION/" "$BASE/$VERSION/" ;;
  *) exit 1 ;;
esac

NDJSON events

--json prints one event per line with stable shapes, ready for jq:

$ nxr up dist/1.4.0/ https://nexus.example.com/repository/raw-main/1.4.0/ --json
{"download":[],"event":"plan","skip":[],"upload":["app-1.4.0.zip","bom/linux-x86_64.json","manifest.json","pinned.xml"]}
{"done":0,"event":"artifact","name":"app-1.4.0.zip","state":"uploading","total":98304}
{"done":0,"event":"artifact","name":"manifest.json","state":"uploading","total":76}
{"done":0,"event":"artifact","name":"bom/linux-x86_64.json","state":"uploading","total":8192}
{"done":0,"event":"artifact","name":"pinned.xml","state":"uploading","total":512}
{"done":512,"event":"artifact","name":"pinned.xml","state":"uploading","total":512}
{"done":16384,"event":"artifact","name":"app-1.4.0.zip","state":"uploading","total":98304}
{"done":76,"event":"artifact","name":"manifest.json","state":"uploading","total":76}
{"done":8192,"event":"artifact","name":"bom/linux-x86_64.json","state":"uploading","total":8192}
{"done":76,"event":"artifact","name":"manifest.json","state":"done","total":76}
{"done":512,"event":"artifact","name":"pinned.xml","state":"done","total":512}
{"done":8192,"event":"artifact","name":"bom/linux-x86_64.json","state":"done","total":8192}
{"done":98304,"event":"artifact","name":"app-1.4.0.zip","state":"done","total":98304}
{"downloaded":0,"event":"summary","kind":"transfer","failed":[],"failures":[],"skipped":0,"uploaded":4}

The summary event is what a job gates on. A re-run of the same job converges instead of transferring:

$ nxr up dist/1.4.0/ https://nexus.example.com/repository/raw-main/1.4.0/ --json \
  | jq -c 'select(.event=="summary")'
{"downloaded":0,"event":"summary","kind":"transfer","failed":[],"failures":[],"skipped":4,"uploaded":0}

uploaded: 0 with everything skipped is the converged signature, and failed is the list to be empty.

The event kinds:

Event Shape
plan {"event":"plan","upload":[…],"download":[…],"skip":[…]}
artifact {"event":"artifact","name":"…","state":"uploading","done":0,"total":123}
retrying {"event":"retrying","name":"…","attempt":2,"reason":"…"}
summary {"event":"summary","kind":"transfer","uploaded":0,"downloaded":0,"skipped":0,"failed":[],"failures":[]}

Artifact states: uploading, downloading, done, skipped. Byte progress is coalesced (at most one event per 200 ms per name), so a slow link does not flood the log. Retries surface as retrying events. Here a server answered 503 twice before cooperating:

$ nxr up dist/1.4.0/ https://nexus.example.com/repository/raw-main/1.4.0/ --json \
  | jq -c 'select(.event=="retrying" and .name=="manifest.json")'
{"attempt":2,"event":"retrying","name":"manifest.json","reason":"transport: https://nexus.example.com/repository/raw-main/1.4.0/manifest.json: HTTP 503"}
{"attempt":3,"event":"retrying","name":"manifest.json","reason":"transport: https://nexus.example.com/repository/raw-main/1.4.0/manifest.json: HTTP 503"}

head, sha and channel get are different: they print one JSON object, not a stream. put and get -o print a transfer's event stream and end with their final object (see the CLI reference).

A release job, end to end

publish:
  stage: deploy
  script:
    - export NXR_USERNAME=ci-bot NXR_PASSWORD="$SECRET_TOKEN"
    - V="1.$CI_PIPELINE_ID.0"
    - make "dist/$V"
    - nxr up "dist/$V/" "https://nexus.example.com/repository/raw-main/$V/" --json > upload.ndjson
    - jq -e 'select(.event=="summary") | .failed == []' upload.ndjson > /dev/null
    - nxr channel set https://nexus.example.com/repository/raw-main/latest "$V" --if-forward

There is no marker step: up generates and uploads the .sha256 siblings itself. The up line is re-runnable as-is: a retried job uploads only what is missing. --if-forward rejects a backward token, so an older pipeline cannot roll latest back.

A consumer job

fetch:
  stage: build
  script:
    - export NXR_USERNAME=ci-bot NXR_PASSWORD="$SECRET_TOKEN"
    - BASE=https://nexus.example.com/repository/raw-main
    - V=$(nxr channel get "$BASE/latest")
    - nxr down "$BASE/$V/" vendor/app/ --names-from names.txt --json | jq -e 'select(.event=="summary") | .failed == []'
    - nxr verify vendor/app/

A retried job picks up the part files of the killed one by default and resumes them through Range requests, so the retry needs no flag. The offline verify is the gate before anything links against the download: no network, pure hashing.

Check the setup before the run

doctor verifies credentials, settings and reachability without printing secrets:

$ nxr -u deployer:pw doctor https://nexus.example.com/repository/raw-main/1.4.0/manifest.json
doctor:
  [  ok  ] credentials: resolved from -u flag
  [  ok  ] tls: verification is ON
  [  ok  ] settings: workers 8, retry 4, stall 30s, connect 15s
  [  ok  ] probe: HEAD https://nexus.example.com/repository/raw-main/1.4.0/manifest.json → HTTP 200
  all checks passed

Without credentials the gate still passes against a server that serves anonymous reads: doctor reports the missing credentials as a warning and exits 0. The warning is a nudge, not a failure: a pipeline that intentionally talks to a read-open repository stays green.

Credentials that the server rejects are a different story: the probe answers 401 or 403, doctor fails it with the rejected status in the detail (HEAD <url> → HTTP 401), and the run exits 3. That is the cheap preflight for a typo'd password.

The exit code says which side to fix: 2 is the job definition, 3 is the server, the network or the credentials (an unreachable server probes as transport and exits 3). Run it at the top of a pipeline.

Next steps

Run nxr from a workflow: the action

This repository ships a composite action that installs the prebuilt nxr from the Releases and runs one command:

- uses: <owner>/nexus-raw@vX.Y.Z
  with:
    command: up
    args: ./dist/ https://nexus.example.com/repository/raw-main/1.4.0/
    auth: ${{ secrets.NXR_AUTH }}   # user:password, exported as NXR_USERNAME/NXR_PASSWORD

The version input defaults to latest, the newest release; pass the action tag's number (with: version: X.Y.Z, without the leading v) to pin the binary to the same release as the action. The binary is checksum-verified against the release SHA256SUMS before it runs, and the action fetches the release of its own repository, not of the caller's workflow repository. Linux (x64, aarch64), macOS (x64, aarch64) and windows-x64 runners are supported.