JFrog DevOps
Instructions
Step 1: Identify the DevOps Operation
| Operation | JFrog Tool | API/CLI | Auth Required |
|---|---|---|---|
| Upload/deploy artifact | Artifactory | PUT /{repo}/{path} or jf rt upload | Yes |
| Download artifact | Artifactory | GET /{repo}/{path} or jf rt download | Yes (unless anonymous) |
| Search artifacts | Artifactory | AQL or jf rt search | Yes |
| Docker push/pull | Artifactory | Docker API or jf docker | Yes |
| Publish build info | Artifactory | PUT /api/build or jf rt build-publish | Yes |
| Promote build | Artifactory | POST /api/build/promote | Yes (admin) |
| Scan for CVEs | Xray | POST /api/v1/scanArtifact or jf xr scan | Yes |
| Create watch/policy | Xray | POST /api/v2/watches | Yes (admin) |
| Generate report | Xray | POST /api/v1/reports/vulnerabilities | Yes |
| Export SBOM (SPDX or CycloneDX) | Xray | POST /xray/api/v2/component/exportDetails, or jf scan --format=cyclonedx (CycloneDX only) | Yes |
| Vet OSS packages before download | Curation | Configured per remote repo | Yes (admin) |
| Manage ML model (Hugging Face, MLflow, NIM) | Artifactory ML repo | jf rt upload or FrogML SDK | Yes |
| Cleanup old artifacts | Artifactory | AQL + delete or retention policies | Yes (admin) |
Step 2: Configure Authentication
Option A: JFrog CLI (recommended):
# Configure JFrog CLI with access token (recommended)
jf config add my-server \
--url="https://acme.jfrog.io" \
--access-token="YOUR_ACCESS_TOKEN" \
--interactive=false
# Verify connection
jf rt ping
Option B: REST API with curl:
# Read the host and token from the environment so neither is hardcoded
# or left in shell history. JF_URL is your platform base URL.
export JF_URL="https://acme.jfrog.io"
# Using access token (recommended)
curl -H "Authorization: Bearer $JF_ACCESS_TOKEN" \
"$JF_URL/artifactory/api/system/ping"
# Using identity token (reference token, also works as Bearer)
curl -H "Authorization: Bearer $JF_REFERENCE_TOKEN" \
"$JF_URL/artifactory/api/system/ping"
Legacy API keys (
X-JFrog-Art-Apiheader) reached end of life in Q4 2024 and are disabled by default in Artifactory 7.98+. Use access tokens or reference tokens (both sent asAuthorization: Bearer).
Option C: OIDC for CI (no long-lived secrets):
# GitHub Actions example with jfrog/setup-jfrog-cli
- uses: jfrog/setup-jfrog-cli@v5
with:
oidc-provider-name: my-github-oidc-provider
env:
JF_URL: https://acme.jfrog.io
v5 runs on the node24 Actions runtime. v4 stays on node20 and is still supported, so pin
@v4only if your runner is held back; otherwise use@v5. Configure the OIDC integration once in JFrog (Administration > Identity and Access > Integrations > OIDC), then CI jobs exchange a short-lived JWT for an access token at runtime. JFrog-recommended path for GitHub Actions, GitLab, Buildkite, and Jenkins.
Option D: Python client:
import requests
class ArtifactoryClient:
"""Client for JFrog Artifactory REST API."""
def __init__(self, base_url, access_token):
self.base_url = base_url.rstrip('/')
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
})
def ping(self):
"""Health check."""
r = self.session.get(f"{self.base_url}/api/system/ping")
return r.text == "OK"
def list_repos(self, repo_type=None):
"""List repositories, optionally filtered by type."""
params = {}
if repo_type:
params["type"] = repo_type
r = self.session.get(f"{self.base_url}/api/repositories", params=params)
return r.json()
def deploy_artifact(self, repo_key, path, file_path, properties=None):
"""Deploy (upload) an artifact to a repository."""
url = f"{self.base_url}/{repo_key}/{path}"
if properties:
prop_str = ";".join(f"{k}={v}" for k, v in properties.items())
url += f";{prop_str}"
with open(file_path, "rb") as f:
r = self.session.put(url, data=f,
headers={"Content-Type": "application/octet-stream"})
return r.json()
def download_artifact(self, repo_key, path, dest_path):
"""Download an artifact from a repository."""
r = self.session.get(f"{self.base_url}/{repo_key}/{path}", stream=True)
with open(dest_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
return dest_path
def search_aql(self, aql_query):
"""Search using Artifactory Query Language."""
r = self.session.post(
f"{self.base_url}/api/search/aql",
data=aql_query,
headers={"Content-Type": "text/plain"}
)
return r.json()
def get_build_info(self, build_name, build_number):
"""Get build information."""
r = self.session.get(f"{self.base_url}/api/build/{build_name}/{build_number}")
return r.json()
def promote_build(self, build_name, build_number, target_repo,
status="released", copy=False):
"""Promote a build to a target repository."""
r = self.session.post(
f"{self.base_url}/api/build/promote/{build_name}/{build_number}",
json={
"status": status, "targetRepo": target_repo,
"copy": copy, "artifacts": True, "dependencies": False
}
)
return r.json()
Step 3: Docker Registry Operations
Configure Docker to use Artifactory:
# Login to the Artifactory Docker registry.
# The registry hostname depends on how the platform is reached:
# SaaS / subdomain method: acme.jfrog.io/<repo-key>
# repository-path method: <host>/artifactory/api/docker/<repo-key>
# port method (self-hosted): <host>:<port>
# Only the first form is shown below. On self-hosted Artifactory WITHOUT a
# reverse proxy configured for the subdomain method, the short form does not
# resolve and you must use one of the other two.
docker login acme.jfrog.io
# Push image through Artifactory
docker tag myapp:latest acme.jfrog.io/docker-local/myapp:1.0.0
docker push acme.jfrog.io/docker-local/myapp:1.0.0
# Pull image through Artifactory (also caches remote images)
docker pull acme.jfrog.io/docker-remote/nginx:latest
jf config adddoes not log the Docker daemon in. It configures the JFrog CLI's own credential store. Under OIDC (Option C) there is no long-lived password to feeddocker loginat all, which is the point of OIDC. Three working routes: runjf docker push/pull, which proxies the daemon using the CLI's configuration; or export the exchanged access token and pipe it,echo "$JF_ACCESS_TOKEN" | docker login acme.jfrog.io -u <username> --password-stdin; or letjfrog/setup-jfrog-cliexport the token into the job and use the same pipe. Never put the token in thedocker login -pargument, which lands it in argv and in shell history.
Using JFrog CLI for Docker (adds build info):
# Push with build info collection
jf docker push acme.jfrog.io/docker-local/myapp:1.0.0 \
--build-name=myapp-build --build-number=42
# Pull with build info collection
jf docker pull acme.jfrog.io/docker-remote/nginx:latest \
--build-name=myapp-build --build-number=42
Step 4: Build Info and Promotion
Publish build info from CI pipeline:
# Collect environment variables
jf rt build-collect-env myapp-build 42
# Upload artifacts with build info
jf rt upload "target/*.jar" libs-release-local/com/acme/myapp/1.0.0/ \
--build-name=myapp-build --build-number=42
# Publish build info
jf rt build-publish myapp-build 42
# Promote build from staging to release
jf rt build-promote myapp-build 42 libs-release-local \
--status="released" --copy
Promotion pipeline pattern:
[Build] -> libs-snapshot-local (dev)
-> libs-staging-local (QA approved)
-> libs-release-local (production ready)
Step 5: Xray Security Scanning
Using JFrog CLI for scanning:
# Audit current project dependencies
jf audit --watches "prod-security-watch"
# Scan a specific Docker image
jf docker scan acme.jfrog.io/docker-local/myapp:1.0.0
# Real CI gate on the SOURCE tree: the policy comes from an Xray watch,
# not from --min-severity
jf audit --watches=prod-security-watch --fail=true # exit code 3 when a Fail Build rule matches
# Real CI gate on the BUILD you just published (this is the one most pipelines want)
jf build-scan my-build 42 --fail --vuln
# --min-severity only filters what is DISPLAYED. Without --watches, --project or
# --repo-path no policy violations are evaluated at all, so this gates nothing:
jf audit --min-severity=High
# Generate SBOM in CycloneDX (with VEX data from Xray 3.67+)
jf scan --format=cyclonedx --sbom "build/libs/*.jar" > sbom.cdx.json
--failalready defaults to true, so passing it changes nothing on its own. The flag that makes a gate real is--watches,--project, or--repo-path, and those three are mutually exclusive:--watchesis documented as "Incompatible with --project and --repo-path", and each of the other two is accepted "only if" the other two are absent. Passing two is a hard CLI error, not a narrowing.
jf auditandjf build-scangate different objects.auditresolves the source tree's declared dependencies.build-scan <name> <number>scans the build info and the artifacts the pipeline actually published, which is what catches a CVE that lives in a shaded, vendored or base-image layer and never appears in your manifest. Publish build info (Step 4), then gate withbuild-scan. Its flags are--fail,--vuln,--violations,--rescan,--trigger-scan-retries,--format,--project.What silently turns a gate into a no-op, in rough order of how often it bites:
- The target repository is not indexed by Xray. Nothing is indexed by default; see the indexing note below. An unindexed repo yields zero violations and a green build.
- The watch exists but is not active, or its policy has no rule with
fail_buildset.block_downloadis a different action and does not fail a build.- The watch's resources do not actually include the repo or build being scanned.
- An ignore rule or waiver already suppresses the violation.
--format=cyclonedxis set: the CLI's own flag help warns that the CycloneDX format carries vulnerabilities, not violations, so policy context is lost.--vulnis set, which reports all vulnerabilities regardless of policy.- SAST, IaC and contextual analysis are JFrog Advanced Security entitlements, so a structurally correct gate can still be scanning less than you assume.
- Exit code 3 is swallowed by
continue-on-error,|| true, or a pipe. Check that the step actually fails.Xray indexing is a prerequisite, not a troubleshooting step. On a new repository Xray indexes nothing until you add it under Indexed Resources, and only supported package types are indexed at all. Verify the repo is indexed AND that the specific artifact has been scanned (
POST /xray/api/v1/artifact/status, orscan-statusin the bundledxray_client.py) before you treat an empty result as clean. Note the asymmetry:jf auditneeds no indexing because it reads your manifests, whilejf docker scan,jf build-scan, watches and the summary API all do. CycloneDX is the only SBOM format JFrog CLI emits. There is nojf scan --format=spdx; the CLI rejects it, and its own help for the enrich command says "Input must be CycloneDX JSON; SPDX or other formats are not accepted." SPDX comes only from the Xray UI (Scans List, then More Options, then Export Scan Data) or fromPOST /xray/api/v2/component/exportDetailswith"spdx": trueplus"spdx_format"(jsonortag-value), which is required wheneverspdxis selected; the call returns 400 if you send no content selector at all.jf docker scanaccepts the SAME--formatvalues asjf scan(table, json, simple-json, sarif, cyclonedx) because both commands share one flag definition, so you can emit an image SBOM directly fromjf docker scan --format=cyclonedx.--sbomis a BOOLEAN display toggle, not an input selector: it makes the output list every SBOM component rather than only the affected ones, and it is ignored unless--formatistableorcyclonedx. The scan target is still the positional path argument.
CBOM here means Cryptography Bill of Materials, not a secrets report. With JFrog Advanced Security, the CBOM option enriches a CycloneDX SBOM with the cryptographic assets that secrets scanning found, embedding each as a cryptographic-asset component (certificates, API and secret keys, generic secrets). It is an enrichment of the SBOM, not an export of the secrets findings. Xray also ingests external SPDX and CycloneDX SBOMs (including VEX contextual analysis) for vetting third-party artifacts.
Frogbot for pull-request scanning:
# .github/workflows/frogbot-scan-pr.yml
- uses: jfrog/frogbot@v3
env:
JF_URL: ${{ secrets.JF_URL }}
JF_ACCESS_TOKEN: ${{ secrets.JF_ACCESS_TOKEN }}
JF_GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Frogbot scans PRs and comments on findings, and can open fix PRs. Frogbot itself is open source and free to run, but the scanners are not uniform: SCA needs a JFrog Platform connection, while SAST, IaC and contextual analysis are JFrog Advanced Security features. Confirm which of them your subscription actually entitles you to before promising a PR gate. It is still a good first step for an Israeli OSS project. Pin @v3: V3 scans statically without executing your package managers, so it still returns results when the build fails, and it auto-detects nested and multi-package repos. V2 is in sunset mode (critical bug and security fixes only, no new features).
Step 5b: AI / ML Model Management (JFrog ML + AI Catalog)
JFrog ML (March 2025, from the Qwak acquisition) and the AI Catalog (September 2025) extend Artifactory and Xray to ML models. The Machine Learning repository type stores Hugging Face models alongside PyTorch, ONNX, .pkl, .joblib, .pth, and .cbm files in one format-agnostic repo, with FrogML SDK support. 7.111.1 is not a minimum: Hugging Face repositories are supported from Artifactory 7.77, and 7.111.1 is the version from which all new local and remote Hugging Face repositories use the Machine Learning layout by default. Xet protocol support is documented for Hugging Face repositories specifically.
# Configure an ML repo (admin UI or REST):
# Administration > Repositories > Add Repository > Local > Machine Learning
# Upload a model artifact with build info
jf rt upload "model.onnx" ml-local/myapp/v1.0.0/ \
--build-name=ml-build --build-number=42
# Scan model files for embedded code-execution payloads (needs JFrog Advanced Security)
jf malicious-scan --working-dirs=./models
# Binary-scan a model artifact you already have on disk
jf scan ./model.onnx --format=sarif
Model scanning is not the Docker path.
jf docker scanresolves images through the local Docker daemon, so pointing it at an ML repository path fails.jf malicious-scan(beta) targets the pickle-deserialization attack class that container scanning does not cover.
The AI Catalog lets central platform teams curate access to OpenAI, Anthropic, NVIDIA NIM (including Nemotron open-weight models), and Hugging Face models behind one governance layer: scanning, lineage, model cards, and one-click deployment.
Step 6: AQL (Artifactory Query Language) Patterns
Common AQL queries for artifact management:
// Find artifacts created in last 7 days
items.find({"created": {"$last": "7d"}, "repo": "libs-release-local"})
// Find Docker images by name
items.find({
"repo": "docker-local",
"path": {"$match": "myapp/*"},
"name": "manifest.json"
}).include("repo", "path", "name", "created", "size")
// Find artifacts larger than 100MB
items.find({
"size": {"$gt": 104857600},
"repo": {"$match": "libs-*-local"}
}).sort({"$desc": ["size"]})
// Find unused artifacts (not downloaded in 90 days)
items.find({
"stat.downloaded": {"$before": "90d"},
"repo": "libs-release-local"
})
// Find artifacts by property
items.find({
"@build.name": "myapp-build",
"@build.number": "42"
})
Examples
Example 1: Set Up Maven Repository
User says: "Set up a Maven repository structure in Artifactory" Result: Create local repo (libs-release-local, libs-snapshot-local), remote repo (jcenter-remote pointing to Maven Central), virtual repo (libs aggregating local + remote), configure resolution and deployment.
Example 2: Docker CI/CD Pipeline
User says: "Integrate Artifactory as Docker registry in our CI pipeline" Result: Configure Docker virtual repository, set up docker login in CI, push images with build info using jf docker push, scan with Xray, promote from staging to production.
Example 3: Security Gate
User says: "Block deployment of artifacts with critical CVEs" Result: Create Xray security policy blocking critical CVEs, create watch on production repositories, configure fail_build action for CI integration, set up violation notifications.
Example 4: Storage Cleanup
User says: "Clean up old artifacts to free Artifactory storage" Result: Use AQL to find artifacts not downloaded in 90+ days, identify snapshot artifacts older than 30 days, create cleanup script with dry-run mode, schedule regular cleanup.
Bundled Resources
Scripts
scripts/artifactory_client.py: JFrog Artifactory REST API client covering health checks, repository listing and creation, artifact upload / download / delete, AQL search, property management, build info retrieval, and build promotion. Reads the token fromJFROG_ACCESS_TOKENonly, never from argv. Promotion copies by default;--moveis opt-in. Requiresrequests(pip3 install requests). Run:python3 scripts/artifactory_client.py --helpscripts/xray_client.py: JFrog Xray REST API client.summaryREADS an existing scan result and does not scan;trigger-scanstarts one and takes a component ID (docker://image:tag), not a repo path;scan-statustells "scanned and clean" apart from "never scanned", which an empty summary cannot. Also covers policy and watch management, violation search and report generation. Reads the token fromJFROG_ACCESS_TOKENonly, never from argv. Requiresrequests(pip3 install requests). Run:python3 scripts/xray_client.py --help
References
references/domain-checklist.md: The coverage contract for this skill, including a dated record of what it deliberately does not cover yet and why. Read it before assuming a topic is handled.references/api-reference.md: Quick reference for Artifactory and Xray REST API endpoints organized by category (system, repositories, artifacts, search, properties, build info, scanning, policies, violations), JFrog CLI command cheatsheet, AQL query patterns, repository type explanations, and standard repository layout conventions. Consult when constructing API calls, writing AQL queries, or setting up repository structures.
Gotchas
- JFrog Pipelines reached end of life on May 1, 2026. New customers cannot provision Pipelines and existing customers must already be migrated. JFrog recommends GitHub Actions, GitLab CI, Jenkins, or Azure DevOps with the
jfrog/setup-jfrog-cliaction/integration. If an Israeli team is still on Pipelines, treat the migration as overdue: no feature updates and no support are available. - The legacy Hugging Face migration deadline passed in June 2026. Any Hugging Face repository still on the legacy layout, which is every one created before Artifactory 7.111.1 made the new layout the default, that has not been moved to the "Machine Learning" layout is now running unsupported, and full functionality is no longer guaranteed. Treat an unmigrated repo as an incident, not a backlog item. Migration is effectively one-way (the
restore_layoutAPI deletes packages added after the upgrade), federated repos cannot mix layouts, and Hugging Face Hub rate limits spike during cache-warming, so plan the cutover rather than running it ad hoc. - API keys reached end of life Q4 2024. Legacy keys still work on older instances but new keys cannot be generated. Migrate any
X-JFrog-Art-Apiusage to access tokens or reference tokens (both sent asAuthorization: Bearer ...). - OIDC is now the JFrog-recommended GitHub Actions auth method. Requires JFrog CLI 2.75.0+ and the workflow needs
permissions: id-token: write. Long-lived access tokens stored in GitHub secrets are still supported but discouraged for new pipelines. - In-country JFrog Cloud regions: ask, do not assume. JFrog no longer publishes a public cloud-region list. The page this skill previously cited now renders empty,
jfrog.com/cloud-service-providers-and-regions/returns 404, and the hosting-models region page 301s to a self-managed architecture page with no regions on it (all three re-checked 2026-09-02). We therefore cannot confirm from any official source which, if any, Israeli regions JFrog SaaS offers. Do not tell an Israeli buyer that data residency is available on JFrog SaaS without confirming it with JFrog directly. The structural point still holds and is worth raising in procurement: the region is fixed when the subscription is created and cannot be changed later without a migration, so settle residency before onboarding rather than after. - Cloud tier pricing transparency varies. JFrog publishes Pro at about $150/month and Enterprise X at about $950/month for SaaS, with Enterprise+ on quote. Self-managed pricing is not published at all; the figures that circulate for it come from third-party trackers we could not verify, so do not quote one. Israeli buyers should get current pricing directly from JFrog Israel before architecting.
- JFrog is an Israeli company, founded and headquartered in Netanya (NASDAQ: FROG) with a substantial R&D presence in Israel. For an Israeli team that means local-timezone enterprise support, a local sales and solution-architect org, and Hebrew-speaking SEs. That is a genuine procurement argument on its own. It does NOT extend to data residency: see the region bullet above, which we could not confirm from any official source.
- A license violation is not always your policy talking. Before treating one as a real blocker, check which policy the watch actually inherited: a permissive internal policy can still surface violations from a banned-license list that came in with a parent template you did not author. We have not verified how Xray models license identity internally, so read the policy rather than assuming the mechanism.
Reference Links
| Source | URL | What to Check |
|---|---|---|
| Artifactory REST API | https://docs.jfrog.com/artifactory/reference | Endpoints, query syntax, AQL. The machine-readable index is https://docs.jfrog.com/artifactory/llms.txt |
| Xray Documentation | https://jfrog.com/xray/ | Vulnerability scanning, license compliance, policies, SBOM/VEX |
| JFrog CLI Releases | https://github.com/jfrog/jfrog-cli/releases | Latest CLI version (2.122.0 as of August 2026), changelog |
| JFrog Docker Registry | https://jfrog.com/help/r/jfrog-artifactory-documentation/docker-repositories | Docker image management, Docker Hub proxy |
| JFrog ML | https://jfrog.com/jfrog-ml/ | MLOps platform (from Qwak acquisition), model registry, FrogML SDK |
| JFrog AI Catalog | https://jfrog.com/press-room/jfrog-launches-ai-catalog-to-secure-and-govern-ai-model-delivery/ | Governance for OpenAI, Anthropic, NVIDIA NIM, Hugging Face |
| Machine Learning Repositories | https://jfrog.com/help/r/jfrog-artifactory-documentation/log-hugging-face-models | New ML repo layout, June 2026 HF migration |
| JFrog Curation | https://jfrog.com/curation/ | OSS package vetting, Compliant Version Selection, and MCP tools for Curation waiver management |
| Frogbot | https://github.com/jfrog/frogbot | Free PR-scanning bot, SCA + SAST + IaC. V3 is current, V2 is sunset |
| OIDC + GitHub Actions | https://jfrog.com/help/r/jfrog-platform-administration-documentation/configure-jfrog-platform-oidc-integration-with-github-actions | Recommended auth for CI, requires CLI 2.75.0+ |
| Pipelines End of Life | https://docs.jfrog.com/releases/docs/pipeline-deprecation-end-of-life | May 1 2026 EOL, migration guidance |
| JFrog Cloud hosting models | https://docs.jfrog.com/installation/docs/system-architecture | Hosting model overview. Note: the former public region list is no longer published, so confirm region availability with JFrog directly |
| Xray SBOM Export API | https://docs.jfrog.com/security/reference/export-component-details-v1-deprecated_components-v2-openapi | POST /xray/api/v2/component/exportDetails, the only programmatic SPDX path |
Troubleshooting
Error: "401 Unauthorized" on API calls
Cause: Invalid or expired access token, or insufficient permissions Solution: Generate a new access token in JFrog UI (Administration then Identity and Access then Access Tokens). Verify the token has the required permissions for the operation. API keys are being deprecated -- prefer access tokens.
Error: "Docker push fails with unknown blob"
Cause: Docker client layer push failed or network interruption Solution: Retry the push. If persistent, check Artifactory storage backend health. Ensure the Docker repository accepts the image architecture (linux/amd64 vs arm64). Check max upload size in Artifactory settings.
Error: "Xray scan shows no results"
Cause: Xray indexing not enabled for the repository, or index not yet complete Solution: Verify Xray is configured to index the target repository (Administration then Xray then Indexed Resources). New repositories need to be explicitly added. Initial indexing of large repositories may take hours.