Skip to content

CLI (oa) ​

oa is a single-binary command-line client for the OpenArtifact REST API: repositories, users, tokens, scans, webhooks, backups, declarative configuration from manifests (oa apply) and the Nexus/Artifactory importer.

Install ​

sh
make build-cli          # builds bin/oa (version injected via -ldflags -X main.version)
go build -o bin/oa ./cmd/oa

Release builds (.goreleaser.yml) ship oa_<version>_<os>_<arch> archives for linux, darwin and windows (amd64, arm64). Source: cmd/oa.

sh
oa version              # prints "oa version <version>" ("dev" for an untagged build)
oa help                 # full command list (also: oa with no arguments, -h, --help)

Server, token and config file ​

SettingFlagEnvDefault
Server URL--server URLOA_SERVERvalue saved by oa auth login, else http://localhost:8080
API token--token TOKENOA_TOKENvalue saved by oa auth login
Output format--format table|jsonn/atable

Precedence: flag, then environment variable, then the config file. Global flags go before the command (oa --format json repo list).

The config file is ~/.config/oa/config.json on every OS (it does not follow XDG_CONFIG_HOME or the platform config dir). It holds {"server": "...", "token": "..."}. The directory is created with mode 0700 and the file is written atomically with mode 0600.

Output: --format json prints indented JSON; anything else prints aligned tables. Errors go to stderr as Error (<status>): <message> and the exit code is 1. HTTP redirects that downgrade https to http are refused, and credentials are stripped when a redirect leaves the original host.

Login ​

sh
oa auth login --server http://localhost:8080      # prompts for username and password (no echo)
oa auth whoami
oa auth logout                                    # clears the saved token locally
printf '%s\n' "$PW" | oa auth logout --revoke --username alice --password-stdin   # also revokes it on the server

Passwords are never accepted on the command line (--password is rejected). Non-interactive:

sh
printf '%s\n' "$PW" | oa auth login --username alice --password-stdin

How it works:

  1. Leave the username blank to paste an existing API token instead.
  2. Otherwise the CLI signs in with POST /api/v1/auth/login on a temporary cookie session. The password is not stored.
  3. If the account must change its password (bootstrap admin, reset account), the CLI asks for a new one first (with --password-stdin: the second stdin line) and signs in again.
  4. It creates an access API token named oa-cli (POST /api/v1/auth/tokens), and saves server and token to the config file.

oa auth logout clears the local token and prints the server-side token id. API tokens cannot revoke tokens, so the oa-cli token stays valid unless you pass --revoke --username U --password-stdin (which signs in and revokes it) or revoke it in the UI under Settings > Access tokens. --revoke needs a login saved by this version of oa. A corrupt ~/.config/oa/config.json no longer blocks oa auth login, help and version; oa auth login rewrites it.

Webhook secrets are read from --secret-file FILE (- for stdin) or OA_WEBHOOK_SECRET, never from argv.

For CI, skip login: create a token in the UI and set OA_SERVER / OA_TOKEN. See authentication for token kinds and scopes.

Commands ​

Commands marked admin need the global manage permission. IDs are the values shown by the matching list/show command.

CommandSubcommands and key flags
authlogin [--server URL] [--username U] [--password-stdin], logout, whoami, passwd [--password-stdin] (stdin: current password, then new, one per line), token list, token create --name N [--kind access|deploy] [--username U] [--password-stdin], token revoke <id> [--username U] [--password-stdin]
repolist, create <name> [--format FMT] [--description D] [--public] [--trash-retention-days N] [--immutable-releases] [--sign-packages] (default format npm), show <name>, update <name> [--description D] [--public[=false]] [--trash-retention-days N|default] [--immutable-releases[=false]] [--sign-packages[=false]] [--merge-proxy-versions[=false]] (only the flags you pass are sent; --sign-packages is for hosted rpm repositories; --merge-proxy-versions is for group repositories and administrators only), delete <name> --confirm <name> (see Deleting a repository), packages <name>, members <name> (the member repositories of a group repository)
trashlist [--repo NAME] [--kind K], restore <kind> <id> [--repo NAME], purge <kind> <id> [--yes], purge --repo NAME --all [--yes] (see Trash)
artifactlist <repo>, upload <repo> <file>, delete <repo> <id> (moved to the trash unless the repository retention is 0)
search[--repo R] [--fmt F] [--limit N] <query> (default limit 50; put flags before the query)
user adminlist, create --username U --email E [--password-stdin] [--full-name N], show <id>, update <id> [--email E] [--full-name N] [--disabled[=false]], delete <id>
group adminlist, create <name> [--description D], show <id>, delete <id>, members <id>, add-member <id> <user-id>, rm-member <id> <user-id>
project (roles: projects)list (with your role), create <name> [--display-name N] [--description D] [--owner USERNAME] (--owner: global admins; others need projects.self_create), show <name> (includes my_role), update <name> [--display-name N] [--description D] (owner; sends PATCH) plus admin-only policy flags --allowed-formats F,F, --allowed-repo-types T,T, --repo-name-prefix P, --allow-public-repos[=false], --max-repositories N, --token-max-ttl-days N, delete <name>, members <name>, add-member <name> (<user-id> | --username U) [--role viewer|developer|maintainer|owner] (default viewer), set-role <name> <user-id> <role> (sends PATCH), rm-member <name> <user-id>, groups <name>, add-group <name> <group-id|group-name> [--role viewer|developer|maintainer], rm-group <name> <group-id|group-name>
token adminlist (all tokens), revoke <id>
gpg adminlist, import --key <armored-file|-> [--name N] [--passphrase-stdin] [--activate|--no-activate], generate [--name N] [--algorithm ed25519|rsa4096] [--user-name U] [--email E] [--comment C] [--activate|--no-activate], activate <key> [--repository R], deactivate <key> [--repository R], export <key> [--output FILE], delete <key> [--force]. <key> is an id, fingerprint or name; the passphrase comes from stdin or OA_GPG_PASSPHRASE, never a flag. See signing
storage adminlist, create --name N [--type filesystem|s3|gcs|azure|garage|seaweedfs|oci] [--config JSON | --config-file FILE|-], show <name>, update <name> [--config JSON | --config-file FILE|-], delete <name>, test <name> (exits 1 when the backend check fails)
backup adminlist, create [--type full|metadata-only], show <id>, download <id> <dest>, restore <archive-file> (uploads an archive written by backup download)
audit adminlist [--limit N] [--action A] [--actor A] (default limit 50; pages through the server's 100-row pages); export [--format jsonl|csv] [--since T] [--until T] [--actor A] [--action A] [-o FILE] streams the whole log, see audit export
webhooklist, create --name N --url U --events E,E [--secret-file F|-] [--active=false], show <id>, update <id> [--name N] [--url U] [--events E,E] [--active[=false]], delete <id>, test <id>, deliveries <id>, repo list <repo>, repo create <repo> --name N --url U --events E,E [--secret-file F|-]
sbomlist <repo>, generate <repo> --artifact-id ID [--format cyclonedx|spdx|spdx-json|spdx3-json] [--spec-version V], show <repo> <id>, download <repo> <id> <dest> [--attest true|bundle], delete <repo> <id>
vexget <repo> [--artifact-id ID] [--format openvex|cyclonedx] [--spec-version V] [--output FILE] [--attest true|bundle], list <repo> [--artifact-id ID], set <repo> --artifact-id ID --vuln CVE --status S [--justification J] [--impact T] [--action T], delete <repo> <id>
dtrack adminshow <repo>, test <repo>, push <repo> --artifact-id ID, pushes <repo>: the repository's Dependency-Track integration (the global settings and API keys are set in Settings > Dependency-Track or the API)
vulnscan <repo> --artifact-id ID --package P --version V --ecosystem E [--scanner osv|trivy|grype], list <repo>, show <repo> <id>, quarantine <repo> <id>, delete <repo> <id>
licensescan <repo> --artifact-id ID, list <repo>, show <repo> <id>
secretscan <repo> --artifact-id ID [--content-file FILE|-], list <repo>, show <repo> <id>, quarantine <repo> <id>, delete <repo> <id>
cleanuplist <repo>, create <repo> --name N (--keep-count N | --keep-days N) [--name-pattern REGEX] [--schedule CRON] [--enabled=false], show <repo> <id>, update <repo> <id> [same flags] (only the flags you pass are sent), delete <repo> <id>, run <repo> <id>
quotashow <repo>, set <repo> [--limit N [--unit MB|GB|TB]] [--max-artifacts N] [--warn-percent P] (default unit GB; at least one flag), delete <repo>
propslist <repo> <artifact-id>, set <repo> <artifact-id> <key> <value>, delete <repo> <artifact-id> <key>, search <repo> <key> <value>
upload<repo> <file|glob>... [--target-path P] [--flat] [--property k=v]... [--build-name N] [--build-number M] [--build-url U] [--vcs-revision SHA]: upload files with build properties, see CI commands
bundlecreate <name> <version> [--description D] [--pipeline P], add <name> <version> (--repo R --path P... | --from-build NAME/NUMBER [--repo R]...), seal <name> <version> [--gpg-key K], show <name> <version>: release bundles for CI, see CI commands
promote<name> <version> --stage S [--target-repo R] [--notes T]: promote a sealed bundle, see CI commands
deltalist <repo> <pkg-id>, get <repo> <pkg-id> --from <sha256> <dest>
apply, diff, export adminapply -f PATH [-R] [--kind K] [--prune] [--prune-all] [--force-secrets] [--continue-on-error] [--validate-only] [-o json], diff (same flags; exits 2 when there are changes), export [--kind K] [--with-members] [-o DIR]: declarative configuration from YAML manifests, see declarative configuration
importnexus|artifactory ..., see Importing
systeminfo (version, status, commit, build time, Go version, OS, arch, uptime, database and storage status; admins get the detailed endpoint, other users only version and status)
version, helplocal only, no server call

Boolean flags use Go syntax: --public, --disabled, and --active=false / --enabled=false to turn off a default-true flag. Update commands send only the flags you pass, so --public=false or --disabled=false change just that field.

auth passwd, auth token create and auth token revoke sign in with your account password on a temporary cookie session (the server refuses these for API-token callers). The password is prompted without echo, or read from stdin with --password-stdin; --username defaults to the saved token's owner.

storage create|update --config is for non-secret settings only: the CLI rejects an inline config containing a key that looks like a credential (secret, password, key, token, cred, connstr, sas), because argv is visible in the process list. Put credentials in a file and pass --config-file FILE, or --config-file - to read the JSON from stdin.

Deleting a repository ​

oa repo delete <name> needs the repository name as confirmation, and the server rejects the request with 400 confirmation_required without it:

sh
oa repo delete old-repo --confirm old-repo     # scripts and CI
oa repo delete old-repo                        # on a terminal: prompts "Type the repository name to confirm:"

--confirm must equal the name; a mismatch fails locally without a request. With no --confirm and stdin not a terminal, the command exits 1 and tells you to pass --confirm <name>. The CLI sends the value as ?confirm=. The repository is moved to the trash for its retention period (default 7 days) instead of being destroyed, unless its retention is 0.

Trash ​

sh
oa trash list                                  # every trashed item, repositories included (admin)
oa trash list --repo my-repo --kind maven_artifact
oa trash restore raw_artifact <id> --repo my-repo
oa trash restore repository <id>               # admin
oa trash purge docker_tag <id>                 # asks "Type 'yes' to confirm"; --yes skips the prompt (admin)
oa trash purge --repo my-repo --all --yes      # empty that repository's trash (repository manage permission)
  • list prints ID, KIND, REPOSITORY, NAME, VERSION, SIZE, DELETED_AT, DELETED_BY and PURGE_AT (all fields with --format json). Without --repo it uses the admin endpoint; with --repo it uses the repository endpoint, which needs only the delete permission on that repository. --kind filters by kind (repository, maven_artifact, npm_version, docker_manifest, ...; the full list is in Trash).
  • restore <kind> <id> restores one item and prints it with --format json. A conflict (the path or version was re-published meanwhile, or the repository is itself in the trash) exits 1 with the server's 409 message. Restoring a repository needs the admin endpoint (no --repo).
  • purge <kind> <id> deletes the item permanently now. Without --yes it asks for confirmation on a terminal and refuses to run when stdin is not one.
  • purge --repo NAME --all purges every trashed item of the repository (DELETE /api/v1/repositories/{name}/trash) and needs the repository manage permission instead of admin; --all requires --repo, and it asks for the same confirmation (--yes skips it). The server purges at most 1000 items per request; the CLI repeats until none remain and prints the totals.
  • oa repo create|update --trash-retention-days N sets the per-repository retention (0 deletes immediately, default on update goes back to the server-wide setting); --immutable-releases[=false] blocks deleting or overwriting released versions. See immutable releases.

Transfers ​

Uploads and downloads stream through the CLI without buffering whole files:

CommandBehavior
oa artifact upload <repo> <file>multipart upload. *.deb goes to POST /api/v1/repositories/{repo}/debs; *.apk goes to .../apks (with --branch, --apk-repo and --arch, see Alpine); any other file goes to .../packages (the RPM upload endpoint). Other formats are published with their native client (npm publish, mvn deploy, curl -T, ...), see formats
oa artifact upload --presigned [--path P] [--content-type T] [--multipart] [--part-size SIZE] [--parallel N] <repo> <file>raw repositories on any storage backend with presigned uploads (S3, GCS, Azure, and filesystem/OCI through the server's signed relay URLs): hashes the file, asks POST /api/v1/repositories/{repo}/uploads for a presigned PUT, uploads the bytes straight to the object store (the OpenArtifact token is never sent there) and calls .../uploads/{id}/complete, which reads the object back from storage, verifies size and sha256 and records the artifact. Files above 5 GiB are uploaded in parts automatically (--multipart forces it, --part-size such as 128M sets the part size, --parallel the parts in flight, default 4, at most 16); each part is retried with backoff and the upload is aborted on the server if a part fails for good. The complete call has no response-header timeout and is retried while the server answers 409 upload_in_progress. --path defaults to the file name. See Presigned URLs
oa backup download <id> <dest>writes to a temp file next to <dest>, syncs, then renames onto <dest>
oa sbom download <repo> <id> <dest>same
oa delta get <repo> <pkg-id> --from <sha256> <dest>same; downloads the delta patch from the given base version

There are no top-level oa upload / oa download commands.

CI commands ​

oa upload, oa bundle and oa promote are made for CI jobs: they print one JSON document with --format json (put the global flags before the command) and exit 0 on success, 1 on any failure, with Error: <message> on stderr. Setup, a non-admin token and plugin usage are in CI integration. They need oa 0.10.0 or newer; oa --format json version prints {"version","go","os","arch"} (version is dev for an unstamped build).

oa upload ​

sh
oa --format json upload <repo> <file|glob>... [--target-path P] [--flat] \
   [--property k=v]... [--build-name N] [--build-number M] [--build-url U] [--vcs-revision SHA]

Uploads to a hosted raw, maven, ivy, sbt, deb, rpm or pacman repository (pacman: the package and its .sig, stored under os/<arch>/ by file name; see pacman) and records properties on every uploaded artifact: build.name, build.number, build.url, vcs.revision from the flags and any --property k=v (a custom property wins over a build flag of the same key). Other formats are refused with a hint to use the native client (npm publish, docker push, twine, ...), and so are proxy and group repositories. Needs read and write on the repository.

Arguments are files or globs (*, ?, [...], and ** for any depth); quote globs to keep the shell out of it. A pattern without a match, a directory or a missing file is an error before anything is uploaded.

Remote path (raw, maven, ivy, sbt; deb and rpm keep the name the server derives from the package and reject --target-path):

FlagsRemote path of dist/a/b.tgz
nonedist/a/b.tgz (the path as given, relative to the working directory)
--flatb.tgz
--target-path rel/1.0/rel/1.0/dist/a/b.tgz (a trailing / makes it a directory prefix)
--target-path rel/1.0/ --flatrel/1.0/b.tgz
--target-path rel/b-1.0.tgz, a single filerel/b-1.0.tgz (exact path)
--target-path rel, several filesrel/<path> (a directory)

An absolute path or one that climbs out of the working directory (..) needs --flat. A Maven, Ivy or sbt path must be the repository layout (com/acme/lib/1.0/lib-1.0.jar).

Every file is hashed locally and the digest the server stored is compared with it (Maven through the .sha256 sidecar); a mismatch fails the upload. Files are uploaded in order and the command stops at the first failure.

json
{
  "uploaded": [
    {
      "repo": "releases",
      "path": "builds/42/dist/app.tar.gz",
      "sha256": "4be899fc...",
      "size": 7,
      "url": "https://oa.example.com/repos/raw/releases/builds/42/dist/app.tar.gz",
      "id": "659e256b-bbb2-4ef7-8daa-b4065fa505e0"
    }
  ]
}

url is the download URL (for deb and rpm the package download endpoint) and id the artifact id used by properties. For deb and rpm path is the local file name; the stored file name, which bundle add --path and the bundle items use, comes from the package metadata. On failure, with --format json, stdout still carries what was uploaded before the error, plus the message:

json
{"uploaded": [ ... ], "error": "unexpected response: PUT /repos/raw/releases/a.txt: HTTP 403: forbidden"}

oa bundle and oa promote ​

sh
oa --format json bundle create <name> <version> [--description D] [--pipeline NAME]
oa --format json bundle add    <name> <version> --repo R --path P [--path P]...
oa --format json bundle add    <name> <version> --from-build NAME/NUMBER [--repo R]...
oa --format json bundle seal   <name> <version> [--gpg-key K]
oa --format json bundle show   <name> <version>
oa --format json promote       <name> <version> --stage S [--target-repo R] [--notes T]
  • create makes a draft bundle; --pipeline attaches an existing pipeline by name.
  • add --repo R --path P... adds artifacts of one repository by their path as oa upload printed it (raw: the path; Maven: the repository path; Ivy and sbt: the repository path; deb and rpm: the stored file name). A path that matches nothing, or more than one artifact, is an error.
  • add --from-build NAME/NUMBER adds every artifact whose properties are build.name=NAME and build.number=NUMBER (split at the last /, so NAME may contain slashes such as a Jenkins folder/job/main). It searches the hosted repositories the token can read, or only the --repo ones (repeatable). It is an error when nothing carries both properties. The same content added twice (by repository and SHA-256) counts once.
  • seal freezes the bundle; --gpg-key signs its manifest with a key id, fingerprint or config (the operator hands the id to the job, the key list is admin-only).
  • promote copies a sealed bundle into the repository of --stage. --target-repo is optional when the pipeline stage is bound to a repository.

All bundle commands print the bundle:

json
{
  "bundle": "shop",
  "version": "1.0.0",
  "state": "sealed",
  "url": "https://oa.example.com/bundles/shop/1.0.0",
  "description": "shop 1.0.0",
  "pipeline_id": "c8bafcff-1f5b-4665-bd51-ab6240c1e1cc",
  "current_stage": "prod",
  "created_by": "ci-bot",
  "sealed_at": "2026-10-24T12:02:14+02:00",
  "added": 3,
  "items": [{"repo": "ci-dev", "path": "builds/42/dist/app.tar.gz", "sha256": "4be899fc...", "size": 7, "format": "raw"}],
  "promoted": {"stage": "prod", "target_repo": "ci-prod", "items": 3, "copied": 3, "existing": 0, "replaced": 0}
}
FieldPresent
bundle, version, state (draft, sealed, archived), urlalways; url is the web UI page <server>/bundles/<name>/<version>, which needs an administrator
description, pipeline_id, current_stage, created_by, sealed_at, gpg_key_idwhen set
addedbundle add: the number of items sent (after removing duplicates)
itemsbundle add (the items it added) and bundle show (all items)
promotedpromote: items = items in the bundle, copied/existing/replaced as in the promotion response

Without --format json each command prints a one-line summary instead.

Permissions ​

None of these commands needs an administrator: a token with read and write on the repositories involved is enough. See CI integration and the bundle permissions.

Importing from Nexus or Artifactory ​

oa import nexus|artifactory --source-url URL --repo SRC[:DST]... copies hosted repositories from a Nexus 3 or Artifactory server: Maven (also Gradle), Debian, RPM, raw, npm, PyPI, Docker/OCI, Helm, NuGet, RubyGems, Cargo, Go, Conda, Conan, Ivy and sbt, with created-at, properties, checksum verification and resume. The source password or token is read from a file, stdin or OA_IMPORT_SOURCE_TOKEN, never from the command line. Flags, supported formats, resume and checksum behavior are in Migrating from Nexus or Artifactory.

Free software under the AGPL-3.0-or-later.