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:
or precompute the compact form once and store it as a single secret:
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¶
- What the producer side publishes: publish a version.
- What the consumer job downloads and how it verifies: consume artifacts.
- Decoding a failure from its
error:line: when it breaks. - The full event and exit-code contract: errors and exit codes.
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.