| name | jfrog-system-config-repo |
| description | Persist and retrieve onboarding manifests via Artifactory or Git. Supports configurable project/repo for Artifactory and clone/commit/push for Git. Routes by state.destination in the manifest. Use when persisting or retrieving onboarding manifests. |
JFrog State Persistence
Prefer jf api per ../../../platform-features/skills/jfrog-cli/jf-api-patterns.md after jf config; curl remains valid fallback.
Manages manifest persistence for onboarding audit trails. Supports two backends:
- Artifactory (default) -- stores manifests in a dedicated JFrog generic repository
- Git -- commits and pushes manifests to a git repository
The backend is determined by state.destination in the manifest (defaults to artifactory).
Inputs (from manifest state section)
| Field | Default | Description |
|---|
state.destination | artifactory | artifactory or git |
state.artifactory.project | system | JFrog project key for manifest storage |
state.artifactory.repository | system-configuration | Generic repo key within the project |
state.git.repo | (required if git) | Git repo in owner/repo format |
state.git.path | / | Directory path in the git repo |
state.git.branch | main | Branch to commit to |
If the state section is missing from the manifest, default to Artifactory with system / system-configuration.
Constants
- Upload path pattern:
<ISO-8601-timestamp>/jfrog-configuration-manifest.yaml
- Timestamp format:
YYYY-MM-DDTHH-MM-SSZ (e.g., 2026-02-17T15-30-00Z)
- Same pattern for both Artifactory and git backends
Security (Artifactory backend)
The Artifactory state project is restricted to platform admins only:
- All
admin_privileges are set to false (no project-level admin can manage members or resources)
- No users or groups are ever added to this project
- Only the platform admin token (used for onboarding operations) has read/write access
- Do not include the state project in the manifest's
jfrog_projects list -- it is internal infrastructure managed by the onboarding agent
This applies to both the default system project and any custom project configured via state.artifactory.project.
Routing
Before any operation, read state.destination from the manifest (default: artifactory):
DESTINATION=$(yq -r '.state.destination // "artifactory"' "$MANIFEST_FILE")
Then branch to the appropriate backend operations below.
Operations
0. Load credentials
if [ -z "$JFROG_URL" ] || [ -z "$JFROG_ACCESS_TOKEN" ]; then
if [ -f .env ]; then
set -a; source .env; set +a
fi
fi
Read configurable parameters
STATE_PROJECT=$(yq -r '.state.artifactory.project // "system"' "$MANIFEST_FILE")
STATE_REPO=$(yq -r '.state.artifactory.repository // "system-configuration"' "$MANIFEST_FILE")
STATE_GIT_REPO=$(yq -r '.state.git.repo // ""' "$MANIFEST_FILE")
STATE_GIT_PATH=$(yq -r '.state.git.path // ""' "$MANIFEST_FILE")
STATE_GIT_BRANCH=$(yq -r '.state.git.branch // "main"' "$MANIFEST_FILE")
GITHUB_HOST=$(yq -r '.github.host // "github.com"' "$MANIFEST_FILE")
Artifactory Backend Operations
Use these when state.destination = artifactory.
Operation A: Ensure Project and Repo Exist (Idempotent)
Run with required_permissions: ["full_network"].
A1. Check/create the project
SC=/tmp/jf-state-proj.code
SB=/tmp/jf-state-proj.json
jf api "/access/api/v1/projects/${STATE_PROJECT}" >"$SB" 2>"$SC"
STATUS=$(tr -d '\r\n' < "$SC")
if [ "$STATUS" = "200" ]; then
echo "OK: State project '${STATE_PROJECT}' already exists"
elif [ "$STATUS" = "404" ]; then
echo "Creating state project '${STATE_PROJECT}'..."
jf api /access/api/v1/projects -X POST \
-H "Content-Type: application/json" \
-d '{
"project_key": "'"${STATE_PROJECT}"'",
"display_name": "System Configuration",
"description": "Internal system configuration and manifest storage. Access restricted to platform admins only.",
"admin_privileges": {
"manage_members": false,
"manage_resources": false,
"index_resources": false
}
}'
echo "OK: State project '${STATE_PROJECT}' created"
else
echo "WARN: Unexpected status $STATUS checking state project"
fi
Access restriction: The project is created with all admin_privileges set to false. This means project-level admins cannot manage members or resources -- only platform admins (using the platform admin token) can read from or write to this project and its repositories.
A2. Verify no unexpected members
If the project already exists, check that no non-admin members have been added:
jf api "/access/api/v1/projects/${STATE_PROJECT}/users" >/tmp/state-users.json 2>/tmp/state-users.code
USERS=$(jq 'length' /tmp/state-users.json)
jf api "/access/api/v1/projects/${STATE_PROJECT}/groups" >/tmp/state-groups.json 2>/tmp/state-groups.code
GROUPS=$(jq 'length' /tmp/state-groups.json)
if [ "$USERS" -gt 0 ] || [ "$GROUPS" -gt 0 ]; then
echo "WARNING: The state project '${STATE_PROJECT}' has $USERS user(s) and $GROUPS group(s) assigned."
echo "This project should only be accessible via platform admin token."
echo "Consider removing non-admin members from the project."
fi
A3. Check/create the repository
SRC=/tmp/jf-state-repo.code
SRB=/tmp/jf-state-repo.json
jf api "/artifactory/api/repositories/${STATE_REPO}" >"$SRB" 2>"$SRC"
STATUS=$(tr -d '\r\n' < "$SRC")
if [ "$STATUS" = "200" ]; then
echo "OK: State repository '${STATE_REPO}' already exists"
elif [ "$STATUS" = "404" ]; then
echo "Creating state repository '${STATE_REPO}'..."
jf api "/artifactory/api/repositories/${STATE_REPO}" -X PUT \
-H "Content-Type: application/json" \
-d '{
"key": "'"${STATE_REPO}"'",
"rclass": "local",
"packageType": "generic",
"projectKey": "'"${STATE_PROJECT}"'",
"description": "Stores onboarding manifests and system configuration. Platform admin access only."
}'
echo "OK: State repository '${STATE_REPO}' created"
else
echo "WARN: Unexpected status $STATUS checking state repo"
fi
Treat 409 (already exists) as success for both project and repository creation.
Operation B: List and Download Existing Manifests (Artifactory)
Run with required_permissions: ["full_network"].
B1. List manifest snapshots
jf api "/artifactory/api/storage/${STATE_REPO}/" >/tmp/state-storage.json 2>/tmp/state-storage.code
CHILDREN=$(jq -r '[.children[]? | select(.folder==true) | .uri] | sort' /tmp/state-storage.json)
MANIFEST_COUNT=$(echo "$CHILDREN" | jq 'length')
echo "Found $MANIFEST_COUNT manifest snapshot(s)"
B2. Get the latest manifest
Sort timestamp folders lexicographically (ISO-8601 sorts naturally) and download the most recent:
jf api "/artifactory/api/storage/${STATE_REPO}/" >/tmp/state-storage2.json 2>/dev/null
LATEST_FOLDER=$(jq -r '[.children[]? | select(.folder==true) | .uri] | sort | last' /tmp/state-storage2.json)
if [ "$LATEST_FOLDER" != "null" ] && [ -n "$LATEST_FOLDER" ]; then
echo "Latest manifest folder: $LATEST_FOLDER"
jf api "/artifactory/${STATE_REPO}${LATEST_FOLDER}/jfrog-configuration-manifest.yaml" >/tmp/latest-manifest.yaml 2>/dev/null
echo "Downloaded to /tmp/latest-manifest.yaml"
else
echo "No manifests found in '${STATE_REPO}' repository"
fi
B3. List all snapshots with timestamps
jf api "/artifactory/api/storage/${STATE_REPO}/" >/tmp/state-storage3.json 2>/dev/null
jq -r '.children[]? | select(.folder==true) | .uri | ltrimstr("/")' /tmp/state-storage3.json
Operation C: Upload Manifest (Artifactory)
Run with required_permissions: ["full_network"].
TIMESTAMP=$(date -u +"%Y-%m-%dT%H-%M-%SZ")
jf api "/artifactory/${STATE_REPO}/${TIMESTAMP}/jfrog-configuration-manifest.yaml" -X PUT \
-H "Content-Type: application/x-yaml" \
--input "$MANIFEST_FILE"
echo "Manifest uploaded to: ${STATE_REPO}/${TIMESTAMP}/jfrog-configuration-manifest.yaml"
Fallback: curl -sf -X PUT "$JFROG_URL/artifactory/${STATE_REPO}/${TIMESTAMP}/jfrog-configuration-manifest.yaml" -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" -H "Content-Type: application/x-yaml" -T "$MANIFEST_FILE"
Where $MANIFEST_FILE is the path to the final manifest YAML file (e.g., ./jfrog-manifest.yaml).
Git Backend Operations
Use these when state.destination = git.
Operation D: Verify Git Repo Accessible
Run with required_permissions: ["all"].
GITHUB_URL="https://${GITHUB_HOST}/${STATE_GIT_REPO}.git"
git ls-remote "$GITHUB_URL" HEAD 2>/dev/null
if [ $? -ne 0 ]; then
echo "ERROR: Cannot access git repository: $GITHUB_URL"
echo "Check that the repo exists and your git credentials are configured."
exit 1
fi
echo "OK: Git repository '$STATE_GIT_REPO' is accessible"
Operation E: List and Download Existing Manifests (Git)
Run with required_permissions: ["all"].
E1. Shallow clone and list snapshots
TMPDIR=$(mktemp -d)
GITHUB_URL="https://${GITHUB_HOST}/${STATE_GIT_REPO}.git"
git clone --depth 1 --branch "$STATE_GIT_BRANCH" "$GITHUB_URL" "$TMPDIR/state-repo" 2>/dev/null
MANIFEST_DIR="${TMPDIR}/state-repo"
if [ -n "$STATE_GIT_PATH" ] && [ "$STATE_GIT_PATH" != "/" ]; then
MANIFEST_DIR="${MANIFEST_DIR}/${STATE_GIT_PATH}"
fi
if [ -d "$MANIFEST_DIR" ]; then
SNAPSHOTS=$(ls -d "$MANIFEST_DIR"/????-??-??T??-??-??Z 2>/dev/null | sort)
MANIFEST_COUNT=$(echo "$SNAPSHOTS" | grep -c . 2>/dev/null || echo 0)
echo "Found $MANIFEST_COUNT manifest snapshot(s) in git"
else
MANIFEST_COUNT=0
echo "No manifest directory found in git repo at path: $STATE_GIT_PATH"
fi
E2. Get the latest manifest
if [ "$MANIFEST_COUNT" -gt 0 ]; then
LATEST=$(echo "$SNAPSHOTS" | tail -1)
if [ -f "$LATEST/jfrog-configuration-manifest.yaml" ]; then
cp "$LATEST/jfrog-configuration-manifest.yaml" /tmp/latest-manifest.yaml
echo "Downloaded latest manifest to /tmp/latest-manifest.yaml"
else
echo "WARNING: Latest snapshot folder exists but manifest file is missing"
fi
fi
E3. Cleanup
rm -rf "$TMPDIR"
Operation F: Upload Manifest (Git)
Run with required_permissions: ["all"].
Clone, add the manifest, commit, and push:
TMPDIR=$(mktemp -d)
GITHUB_URL="https://${GITHUB_HOST}/${STATE_GIT_REPO}.git"
git clone --depth 1 --branch "$STATE_GIT_BRANCH" "$GITHUB_URL" "$TMPDIR/state-repo" 2>/dev/null
TIMESTAMP=$(date -u +"%Y-%m-%dT%H-%M-%SZ")
TARGET_DIR="${TMPDIR}/state-repo"
if [ -n "$STATE_GIT_PATH" ] && [ "$STATE_GIT_PATH" != "/" ]; then
TARGET_DIR="${TARGET_DIR}/${STATE_GIT_PATH}"
fi
TARGET_DIR="${TARGET_DIR}/${TIMESTAMP}"
mkdir -p "$TARGET_DIR"
cp "$MANIFEST_FILE" "${TARGET_DIR}/jfrog-configuration-manifest.yaml"
cd "$TMPDIR/state-repo"
git add -A
git commit -m "Add onboarding manifest snapshot ${TIMESTAMP}"
git push origin "$STATE_GIT_BRANCH"
echo "Manifest pushed to: ${STATE_GIT_REPO} @ ${STATE_GIT_BRANCH}:${STATE_GIT_PATH}/${TIMESTAMP}/jfrog-configuration-manifest.yaml"
cd /
rm -rf "$TMPDIR"
Error Handling
- 409 Conflict: Project or repo already exists -- treat as success (Artifactory)
- 403 Forbidden: Token lacks platform admin privileges -- this should have been caught by prerequisite checks; report to user (Artifactory)
- 404 Not Found on list/download: No manifests exist yet -- not an error, proceed normally
- Git push failure: Warn the user, suggest checking credentials and branch protection rules
- If upload/push fails, warn the user but do not abort the overall onboarding (manifests are a convenience, not a hard requirement)
Usage in the Orchestration Skill
The orchestration skill (jfrog-project-onboarding) calls this skill at two points:
- Pre-flight (Step 0): Read
state.destination from the manifest. For artifactory: call Operation A + B to ensure the repo exists and check for prior manifests. For git: call Operation D + E to verify the repo and check for prior manifests. If found, alert the user and offer to retrieve the latest.
- Post-completion (final step): For
artifactory: call Operation C to upload. For git: call Operation F to commit and push.