| name | jfrog-reconcile-manifest |
| description | Reconcile JFrog Platform state with a desired-state manifest. Reads current configuration, computes a diff, presents changes for approval, and applies only the delta. Use when the user wants to update, sync, or reconcile JFrog with manifest changes. |
JFrog Reconcile Manifest
Prefer jf api (../../../platform-features/skills/jfrog-cli/jf-api-patterns.md) after jf config; curl remains valid fallback for the same paths.
Given a manifest YAML describing the desired state, compare it against the current JFrog Platform state and apply only the differences -- after user approval.
Inputs
manifest -- the desired-state manifest YAML, explicitly provided by the user (file path or parsed structure), following the schema in templates/manifest-template.yaml. This must be a path the user gave directly -- never auto-discover or use locally found manifest files.
baseline -- (optional) a previously stored manifest YAML used as the baseline for comparison. If provided by the caller (e.g., the orchestration skill already downloaded it from the state backend), skip the retrieval in Phase 0. The baseline must always come from the configured state backend (Artifactory or git) -- never from a locally found file.
- All JFrog credentials (
JFROG_URL, JFROG_ACCESS_TOKEN) must be loaded and validated before invoking this skill (handled by the orchestration skill's prerequisite checks)
Overview
The reconciliation operates as a 3-way comparison (similar to terraform plan) when a baseline manifest is available, or a 2-way comparison when it is not:
Baseline Manifest Desired State (manifest) Live State (JFrog APIs)
(last applied state) | |
\ | |
+--- What changed in --------+ |
the manifest |
\ /
+------------ Diff Engine --------------+
|
Change Summary
(manifest changes + drift)
|
User Approval
|
Apply Delta
- Baseline manifest = the last manifest that was successfully applied and persisted to the state backend (Artifactory or git). Think of it as the Terraform state file.
- Desired state = the new/edited manifest the user wants to apply now.
- Live state = the actual current configuration in the JFrog Platform (read from APIs).
When a baseline is available, the diff summary shows:
- Manifest changes: what the user intentionally changed (baseline vs. desired)
- Drift: what changed in the live platform since the last apply (baseline vs. live)
- Net actions: what operations are needed to bring live state to the desired state
When no baseline is available, the skill falls back to a 2-way comparison (desired vs. live), which is the standard behavior.
Phase 0: Retrieve Baseline Manifest (State File)
Before reading live state, check for a previously stored manifest to use as a baseline. This step is skipped if the caller already provides a baseline input.
0.1 Determine state backend
DESTINATION=$(yq -r '.state.destination // "artifactory"' "$MANIFEST_FILE")
0.2 Ask the user about baseline source
Present the user with a choice (use AskQuestion):
To produce an accurate change plan, I can compare your new manifest against a baseline (the last applied manifest stored in your state backend). Should I retrieve it?
Options:
- Retrieve from state backend (
<artifactory|git> -- based on state.destination) -- retrieves the latest stored manifest automatically
- No baseline (skip) -- proceed with live-only comparison (2-way diff)
Important: The baseline must always come from the configured state backend (Artifactory or git). Do not offer or accept locally found files as baselines. Only the user-provided desired-state manifest and the stored state file are used in the comparison.
0.3 Retrieve from state backend
If the user chooses the state backend option, use the jfrog-system-config-repo skill:
Artifactory backend (state.destination = artifactory)
Run with required_permissions: ["full_network"].
STATE_PROJECT=$(yq -r '.state.artifactory.project // "system"' "$MANIFEST_FILE")
STATE_REPO=$(yq -r '.state.artifactory.repository // "system-configuration"' "$MANIFEST_FILE")
jf api "/artifactory/api/repositories/${STATE_REPO}" >/tmp/rec-state-repo.json 2>/tmp/rec-state-repo.code
STATUS=$(tr -d '\r\n' < /tmp/rec-state-repo.code)
if [ "$STATUS" != "200" ]; then
echo "INFO: State repository '${STATE_REPO}' does not exist. No baseline available."
echo "Proceeding with live-only comparison."
BASELINE_FILE=""
else
jf api "/artifactory/api/storage/${STATE_REPO}/" >/tmp/rec-storage.json 2>/dev/null
LATEST_FOLDER=$(jq -r '[.children[]? | select(.folder==true) | .uri] | sort | last' /tmp/rec-storage.json)
if [ "$LATEST_FOLDER" != "null" ] && [ -n "$LATEST_FOLDER" ]; then
jf api "/artifactory/${STATE_REPO}${LATEST_FOLDER}/jfrog-configuration-manifest.yaml" \
>/tmp/baseline-manifest.yaml 2>/dev/null
echo "OK: Retrieved baseline manifest from ${STATE_REPO}${LATEST_FOLDER}"
BASELINE_FILE="/tmp/baseline-manifest.yaml"
else
echo "INFO: No manifest snapshots found in '${STATE_REPO}'. No baseline available."
BASELINE_FILE=""
fi
fi
Git backend (state.destination = git)
Run with required_permissions: ["all"].
Use Operation E from jfrog-system-config-repo to clone the repo and retrieve the latest manifest snapshot. If found, save to /tmp/baseline-manifest.yaml.
0.4 Baseline loaded summary
After retrieval (or skip), report to the user:
- Baseline found: "Using baseline manifest from
<source> (timestamp: <ts>). Changes will be shown as a 3-way comparison."
- No baseline: "No baseline manifest available. Changes will be computed against live JFrog state only (2-way comparison)."
Store the baseline (if any) as BASELINE_FILE for use in Phase 2.
Phase 1: Read Current State
For each project listed in the manifest, query the JFrog Platform to discover what currently exists. All API calls run with required_permissions: ["full_network"].
IMPORTANT: Prefer jf api (../../../platform-features/skills/jfrog-cli/jf-api-patterns.md). Follow the Safe API Call Pattern from the jfrog-platform rule -- never pipe API stdout directly to jq. Capture HTTP status and body separately (jf api writes status to stderr), check the status before parsing. Add sleep 1 between calls in loops to avoid rate limiting.
1.1 Projects
STATUS=$( (jf api "/access/api/v1/projects/$PROJECT_KEY" >/tmp/project-$PROJECT_KEY.json 2>/tmp/project-$PROJECT_KEY.code) ; tr -d '\r\n' < /tmp/project-$PROJECT_KEY.code )
if [ "$STATUS" = "200" ]; then
jq . /tmp/project-$PROJECT_KEY.json
fi
1.2 Repositories
HTTP_CODE=$( (jf api "/artifactory/api/repositories?project=$PROJECT_KEY" >/tmp/repos-$PROJECT_KEY.json 2>/tmp/repos-$PROJECT_KEY.code) ; tr -d '\r\n' < /tmp/repos-$PROJECT_KEY.code )
if [ "$HTTP_CODE" = "200" ]; then
REPO_KEYS=$(jq -r '.[].key' /tmp/repos-$PROJECT_KEY.json)
else
echo "ERROR: Failed to list repos (HTTP $HTTP_CODE)"
fi
for REPO_KEY in $REPO_KEYS; do
HTTP_CODE=$( (jf api "/artifactory/api/repositories/$REPO_KEY" >/tmp/repo-$REPO_KEY.json 2>/tmp/repo-$REPO_KEY.code) ; tr -d '\r\n' < /tmp/repo-$REPO_KEY.code )
if [ "$HTTP_CODE" = "200" ]; then
jq '{key, rclass, packageType, xrayIndex}' /tmp/repo-$REPO_KEY.json
fi
sleep 1
done
1.3 Members
HTTP_CODE=$( (jf api "/access/api/v1/projects/$PROJECT_KEY/users" >/tmp/users-$PROJECT_KEY.json 2>/tmp/users-$PROJECT_KEY.code) ; tr -d '\r\n' < /tmp/users-$PROJECT_KEY.code )
if [ "$HTTP_CODE" = "200" ]; then
CURRENT_USERS=$(cat /tmp/users-$PROJECT_KEY.json)
fi
HTTP_CODE=$( (jf api "/access/api/v1/projects/$PROJECT_KEY/groups" >/tmp/groups-$PROJECT_KEY.json 2>/tmp/groups-$PROJECT_KEY.code) ; tr -d '\r\n' < /tmp/groups-$PROJECT_KEY.code )
if [ "$HTTP_CODE" = "200" ]; then
CURRENT_GROUPS=$(cat /tmp/groups-$PROJECT_KEY.json)
fi
1.4 OIDC (if github.oidc_setup: true)
HTTP_CODE=$( (jf api "/access/api/v1/oidc" >/tmp/oidc-providers.json 2>/tmp/oidc-providers.code) ; tr -d '\r\n' < /tmp/oidc-providers.code )
if [ "$HTTP_CODE" = "200" ]; then
for PROVIDER_NAME in $(jq -r '.[].name' /tmp/oidc-providers.json); do
jf api "/access/api/v1/oidc/${PROVIDER_NAME}/identity_mappings" \
>/tmp/oidc-mappings-$PROVIDER_NAME.json 2>/tmp/oidc-mappings-$PROVIDER_NAME.code
sleep 1
done
fi
1.5 Xray Indexing Status
HTTP_CODE=$( (jf api "/xray/api/v1/binMgr" >/tmp/xray-binmgr.json 2>/tmp/xray-binmgr.code) ; tr -d '\r\n' < /tmp/xray-binmgr.code )
if [ "$HTTP_CODE" = "200" ]; then
BIN_MGR_ID=$(jq -r '.[0].bin_mgr_id // "default"' /tmp/xray-binmgr.json)
else
BIN_MGR_ID="default"
fi
HTTP_CODE=$( (jf api "/xray/api/v1/binMgr/$BIN_MGR_ID/repos" >/tmp/xray-indexed.json 2>/tmp/xray-indexed.code) ; tr -d '\r\n' < /tmp/xray-indexed.code )
if [ "$HTTP_CODE" = "200" ]; then
INDEXED_REPOS=$(jq '.indexed_repos' /tmp/xray-indexed.json)
else
echo "WARN: Could not retrieve Xray indexed repos (HTTP $HTTP_CODE)"
INDEXED_REPOS="[]"
fi
1.6 Curation Status
HTTP_CODE=$( (jf api "/curation/api/v1/repos/${REPO_KEY}" >/tmp/curation-$REPO_KEY.json 2>/tmp/curation-$REPO_KEY.code) ; tr -d '\r\n' < /tmp/curation-$REPO_KEY.code )
if [ "$HTTP_CODE" = "200" ]; then
CURATED=$(jq -r '.curated // false' /tmp/curation-$REPO_KEY.json)
fi
sleep 1
Phase 2: Compute Diff
Compare the manifest (desired state) against the current state gathered in Phase 1. When a baseline manifest is available (from Phase 0), also compare baseline vs. live to detect drift (changes made outside of the manifest workflow).
Diff modes
| Baseline available? | Comparison | What it shows |
|---|
| Yes | Baseline → Desired | What the user intentionally changed in the manifest |
| Yes | Baseline → Live | What drifted in the platform since last apply |
| Yes | Desired → Live | What actions are needed (net result) |
| No | Desired → Live | Standard 2-way diff (current behavior) |
The net actions (Desired → Live) always determine what gets applied. The baseline comparisons are informational, helping the user understand why a change is being made.
Categorize every difference into one of these change types:
2.1 Projects
| Current State | Desired State | Change Type |
|---|
| Does not exist | In manifest | CREATE project |
| Exists | In manifest | NO CHANGE (project already exists) |
| Exists | Not in manifest | FLAG (warn user but do NOT auto-delete) |
2.2 Repositories
For each project, build the desired repo set by:
- Generating expected repos from the manifest
ecosystems list (local + remote + virtual per ecosystem)
- Applying the
repositories list (custom definitions): overrides for matching keys, additions for new keys
- This is the same smart merge logic used by
jfrog-create-repos
Then compare the desired repo set against current repos:
| Current State | Desired State | Change Type |
|---|
| Repo does not exist | In desired set (ecosystem or custom) | CREATE repo |
| Repo exists, config matches | In desired set | NO CHANGE |
| Repo exists, config differs | In desired set with different properties | UPDATE repo config (e.g., description, xrayIndex) |
| Repo exists | Not in desired set | FLAG (extra repo -- warn but do NOT delete) |
For custom repos being created, use inline JSON payloads (same approach as jfrog-create-repos). For overridden repos, merge the custom fields into the existing config.
2.3 Xray/Curation Settings
For each existing repo, compare the current xrayIndex value against the resolved xray_enabled and curation status against curation_enabled:
| Setting | Current | Desired | Change Type |
|---|
| xrayIndex | true | true | NO CHANGE |
| xrayIndex | false | true | ENABLE Xray indexing |
| xrayIndex | true | false | DISABLE Xray indexing |
| curated | false | true | ENABLE Curation |
| curated | true | false | DISABLE Curation |
2.4 Members
Compare current project members against manifest members:
| Current State | Desired State | Change Type |
|---|
| User/group not in project | In manifest members | ADD member |
| User/group in project with same role | In manifest with same role | NO CHANGE |
| User/group in project with different role | In manifest with different role | UPDATE role |
| User/group in project | Not in manifest members | REMOVE member (flag for user approval) |
2.5 OIDC
| Current State | Desired State | Change Type |
|---|
| No provider | oidc_setup: true | CREATE provider + mappings |
| Provider exists, mapping missing | Repo in manifest | CREATE identity mapping |
| Provider exists, mapping exists | Repo in manifest | NO CHANGE |
| Mapping exists | Repo not in manifest | FLAG (extra mapping -- warn) |
| Provider exists | oidc_setup: false | FLAG (warn -- OIDC was previously set up) |
Phase 3: Present Diff
Display a clear, structured summary of all changes to the user. Group by project and use symbols to indicate change type.
3-way presentation (when baseline is available)
When a baseline manifest is available, present the diff in two sections: a manifest changes summary (what the user changed) and a drift summary (what changed in the platform), followed by the planned actions (what will be applied).
Reconciliation Plan:
====================
Baseline: system-configuration/2026-02-17T15-30-00Z (Artifactory)
--- Manifest Changes (what you changed) ---
Project "webapp":
Repositories:
+ ADDED to manifest: go ecosystem (webapp-go-local, webapp-go-remote, webapp-go)
+ ADDED to manifest: webapp-npm-release (custom local/npm)
~ CHANGED in manifest: webapp-npm-local (description updated)
Members:
+ ADDED to manifest: charlie (Developer)
~ CHANGED in manifest: alice role (Contributor -> Developer)
- REMOVED from manifest: bob
Project "newsvc":
+ NEW project added to manifest
--- Drift (changed outside manifest since last apply) ---
Project "webapp":
Repositories:
! webapp-docker-remote: xrayIndex changed (was true, now false) -- modified outside manifest
! webapp-legacy-local: exists in JFrog but was never in any manifest
Members:
[no drift detected]
--- Planned Actions (what will be applied) ---
Project "webapp" (EXISTS):
Repositories:
+ CREATE (ecosystem): webapp-go-local, webapp-go-remote, webapp-go (virtual)
+ CREATE (custom): webapp-npm-release (local/npm)
~ UPDATE: webapp-npm-local (description changed)
[no change]: webapp-npm-remote, webapp-npm, webapp-docker-local, webapp-docker-remote, webapp-docker
Xray:
~ ENABLE indexing on: webapp-go-local, webapp-go-remote, webapp-npm-release
Curation:
[no change]
Members:
+ ADD user: charlie (Developer)
~ UPDATE role: alice (Contributor -> Developer)
- REMOVE user: bob (in JFrog but not in manifest)
OIDC:
[no change]
Project "newsvc" (NEW):
+ CREATE project
+ CREATE repositories: newsvc-npm-local, newsvc-npm-remote, newsvc-npm
+ ENABLE Xray indexing on: newsvc-npm-local, newsvc-npm-remote
+ ADD members: dev-team (Developer)
Flagged items (require explicit confirmation):
! Project "webapp": user "bob" is in JFrog but not in manifest. Remove? (Yes/No)
! Project "webapp": repo "webapp-legacy-local" exists but is not in desired repo set. (No action taken -- informational only.)
! Project "webapp": repo "webapp-docker-remote" has drifted xrayIndex. The desired state will restore it to true.
Proceed with changes? (Yes / No)
2-way presentation (no baseline)
When no baseline is available, skip the "Manifest Changes" and "Drift" sections and show only the "Planned Actions" section:
Reconciliation Plan:
====================
Baseline: none (comparing desired manifest against live JFrog state)
--- Planned Actions ---
Project "webapp" (EXISTS):
Repositories:
+ CREATE (ecosystem): webapp-go-local, webapp-go-remote, webapp-go (virtual)
+ CREATE (custom): webapp-npm-release (local/npm)
~ UPDATE: webapp-npm-local (description changed)
[no change]: webapp-npm-remote, webapp-npm, webapp-docker-local, webapp-docker-remote, webapp-docker
Xray:
~ ENABLE indexing on: webapp-go-local, webapp-go-remote, webapp-npm-release
Curation:
[no change]
Members:
+ ADD user: charlie (Developer)
~ UPDATE role: alice (Contributor -> Developer)
- REMOVE user: bob (in JFrog but not in manifest)
OIDC:
[no change]
Project "newsvc" (NEW):
+ CREATE project
+ CREATE repositories: newsvc-npm-local, newsvc-npm-remote, newsvc-npm
+ ENABLE Xray indexing on: newsvc-npm-local, newsvc-npm-remote
+ ADD members: dev-team (Developer)
Flagged items (require explicit confirmation):
! Project "webapp": user "bob" is in JFrog but not in manifest. Remove? (Yes/No)
! Project "webapp": repo "webapp-legacy-local" exists but is not in desired repo set. (No action taken -- informational only.)
Proceed with changes? (Yes / No)
Change type symbols
| Symbol | Meaning |
|---|
+ | Create / Add |
~ | Update / Modify |
- | Remove |
! | Flagged -- requires explicit user confirmation |
[no change] | Already matches desired state |
Deletion policy
- Projects: Never auto-delete. Only flag as informational.
- Repositories: Never auto-delete. Flag extra repos as informational.
- Members: Prompt user for each member removal individually. If user declines, skip the removal.
- OIDC mappings: Flag extra mappings as informational. Do not auto-remove.
Phase 4: Apply Delta
Only proceed after the user explicitly approves the changes. Execute only the delta operations, reusing existing skills:
4.1 Create new projects
Skill: jfrog-provision-project -- for each project marked as CREATE.
4.2 Create new / update existing repositories
Skill: jfrog-create-repos -- pass ecosystems, repositories, xray_enabled, and curation_enabled as resolved for the project. The skill handles the smart merge logic and creates/updates repos as needed.
For repos that already exist but need config updates (e.g., changed description, xrayIndex toggle):
jf api "/artifactory/api/repositories/$REPO_KEY" -X POST \
-H "Content-Type: application/json" \
-d "$UPDATED_JSON_PAYLOAD"
4.3 Update Xray indexing
For repos where Xray indexing needs to change:
IMPORTANT: The PUT /xray/api/v1/binMgr/{id}/repos endpoint replaces the entire indexed repos list. Sending only new repos will remove all previously indexed repos from the list. Always GET the current list first, merge in new repos (dedup by name), and PUT the combined set.
Enable indexing -- two steps per repo, then a single merged PUT:
Step 1: Set xrayIndex: true on each repo's Artifactory config (add a 1-second delay between calls to avoid rate limiting):
jf api "/artifactory/api/repositories/$REPO_KEY" -X POST \
-H "Content-Type: application/json" \
-d '{"xrayIndex": true}'
sleep 1
Step 2: After all repo configs are updated, merge new repos into the Xray index:
HTTP_CODE=$( (jf api "/xray/api/v1/binMgr/$BIN_MGR_ID/repos" >/tmp/xray-current.json 2>/tmp/xray-current.code) ; tr -d '\r\n' < /tmp/xray-current.code )
if [ "$HTTP_CODE" = "200" ]; then
EXISTING=$(jq '.indexed_repos // []' /tmp/xray-current.json)
else
echo "WARN: Could not GET current Xray index (HTTP $HTTP_CODE), using empty list"
EXISTING="[]"
fi
NEW_REPOS='[{"name":"REPO1","type":"local","pkg_type":"npm"}, ...]'
MERGED=$(echo "$EXISTING" "$NEW_REPOS" \
| jq -s 'add | group_by(.name) | map(.[0])')
echo "{\"indexed_repos\": $MERGED}" >/tmp/xray-merged.json
jf api "/xray/api/v1/binMgr/$BIN_MGR_ID/repos" -X PUT -H "Content-Type: application/json" \
--input /tmp/xray-merged.json >/tmp/xray-put-resp.json 2>/tmp/xray-put-resp.code
HTTP_CODE=$(tr -d '\r\n' < /tmp/xray-put-resp.code)
if [ "$HTTP_CODE" = "200" ]; then
echo "OK: Xray index updated"
else
echo "ERROR: Xray index PUT failed (HTTP $HTTP_CODE)"
fi
Disable indexing (update repo config):
jf api "/artifactory/api/repositories/$REPO_KEY" -X POST \
-H "Content-Type: application/json" \
-d '{"xrayIndex": false}'
4.4 Update Curation
For remote repos where Curation needs to change:
jf api "/curation/api/v1/repos/${REPO_KEY}" -X PUT \
-H "Content-Type: application/json" \
-d '{"curated": true}'
jf api "/curation/api/v1/repos/${REPO_KEY}" -X PUT \
-H "Content-Type: application/json" \
-d '{"curated": false}'
4.5 Add/update/remove members
Skill: jfrog-manage-members -- for adding and updating roles.
For removals (only after individual user approval):
jf api "/access/api/v1/projects/$PROJECT_KEY/users/$JFROG_USER_NAME" -X DELETE
jf api "/access/api/v1/projects/$PROJECT_KEY/groups/$GROUPNAME" -X DELETE
Fallback: same paths with curl -sf -X DELETE and bearer token.
4.6 OIDC changes
Skill: jfrog-oidc-setup -- for creating new providers and mappings.
4.7 GitHub repo configuration
If new ecosystems were added and GitHub repos are configured, the package-manager and CI workflow skills should also be triggered for the new ecosystems:
- Skill:
github-configure-package-managers -- for new ecosystems only
- Skill:
github-configure-ci-workflows -- for new ecosystems only
Error Handling
- 409 Conflict: Resource already exists -- treat as success (idempotent)
- 404 Not Found: Resource does not exist where expected -- include in diff as needing creation
- If any apply operation fails, report the error but continue with remaining changes
- Provide a final summary of all changes applied, skipped, and failed
Post-Reconciliation
After all changes are applied:
- The manifest used for reconciliation becomes the new "current state" manifest
- The orchestration skill persists it via the
jfrog-system-config-repo skill, respecting state.destination:
artifactory: Upload to the configured project/repo (Operation C)
git: Commit and push to the configured git repo (Operation F)
- Report the upload/push location to the user