| name | git-integration-operations-cli |
| description | Automate the Microsoft Fabric Git integration lifecycle from CLI environments using the Fabric CLI (fab api), with az rest as a fallback. Use when the user wants to connect a workspace to Azure DevOps or GitHub, commit or update/pull workspace items, check Git sync status, resolve update conflicts, disconnect, automate Git sync with a service principal, or link a branch workspace to its base. Do NOT use for branch switching, for deployment tools (fab deploy, fabric-cicd, Bulk Import), for deployment-pipeline runtime, or for cross-workspace reference rebinding after promotion (a property of the item definition format, not the Git lifecycle). Triggers: "connect workspace to git", "commit fabric to git", "update workspace from git", "fabric git status", "resolve git conflict fabric", "service principal git sync", "link branch to base".
|
| metadata | {"maturity":"experimental","owner":"yaron"} |
Telemetry — MANDATORY. Every api.fabric.microsoft.com call must carry
x-ms-fabric-skill: git-integration-operations-cli (az rest: --headers "x-ms-fabric-skill=git-integration-operations-cli"),
including every LRO poll, fabric_lro and retry. Snippets omit it — add it anyway.
CRITICAL NOTES
- To find a workspace ID from its name: list all workspaces, then filter by
displayName with JMESPath. To find an item ID: list items of that type in the
workspace, then filter by displayName.
- All Git operations are control plane calls against
https://api.fabric.microsoft.com. Run fab auth login once and fab api
sets the base URL and Fabric token audience automatically. With the az rest
fallback you must pass the matching --resource yourself, or you get a 401.
commitToGit, updateFromGit, and even git/status can return HTTP 202
(long-running operation), not just a synchronous body. On 202, poll the
operation: with fab, read x-ms-operation-id from --show_headers and poll
fab api operations/{id} until status is Succeeded; with the az rest
fallback, poll the Location / x-ms-operation-id header (see COMMON-CORE
long-running operation polling). THEN read git/status to confirm
workspaceHead == remoteCommitHash. Prefer the operation poll to confirm
completion; if you poll git/status instead, treat an in-progress (202) or
not-yet-synced response as "still running" and do not act on its changes
until the heads match. (Learn: rest/api/fabric/core/git and
fabric/cicd/git-integration/git-automation.)
updateFromGit requires the workspace's current workspaceHead; a stale
value returns 400 WorkspaceHeadMismatch. Always read git/status first.
- Prerequisites for every Git operation: the workspace must be assigned to a
capacity (an unassigned workspace fails with
WorkspaceHasNoCapacityAssigned),
and the caller (user or service principal) must hold the right workspace
role: connect and disconnect require Admin; commit and update require at
least Contributor with write permission on all items; branch switching
requires Admin (or Contributor when the workspace opt-in setting Allow users
with at least Contributor role to change Git branch is on). Only one Git
operation may run against a workspace at a time.
- Tenant admin switches gate Git integration and are a common cause of an
unexplained failure. The GitHub sync switch is off by default (enable it
before any GitHub connect); Azure DevOps sync is on by default. Cross-region
workspace-to-Azure-repo needs the cross-geo export switch (GitHub does not
enforce it). See references/git-integration-concepts.md § Tenant admin prerequisites.
Git Integration Operations — CLI Skill
Automate the Fabric Git integration lifecycle (connect, commit, update, status,
disconnect) from CLI environments. Whether cross-workspace item references rebind
after promotion is a separate concern, determined by the item definition format
(logical vs object IDs) rather than the Git lifecycle itself.
Prerequisite Knowledge
Read these shared references first (paths assume this skill lives under
skills/git-integration-operations-cli/ in skills-for-fabric; adjust when drafting
outside the repo):
Table of Contents
Must/Prefer/Avoid
MUST DO
- Read
git/status and pass the current workspaceHead into every commitToGit
and updateFromGit call.
- Poll
git/status after each async operation until
workspaceHead == remoteCommitHash and the changes array is empty. A
202/Succeeded status is not proof of a correct sync — validate the state.
- Deploy an item together with the items it references in the same commit, so
logical IDs can resolve inside the target workspace.
MUST NEVER
- Never run two Git operations concurrently on the same workspace — a second
commitToGit/updateFromGit while one is still Running corrupts head
tracking. Serialize: poll the first to Succeeded before starting the next.
- Never treat a workspace as both a source of truth and a CI/CD push target
on the same branch. Pick one writer per branch: either humans commit from the
workspace, or automation pushes to Git — not both at once.
PREFER
fab api (after fab auth login) as the primary driver: it sets the base
URL and Fabric token audience automatically, avoiding the 401 wrong-audience
trap. Use az rest with --resource "https://api.fabric.microsoft.com" as the
fallback when fab is unavailable or you need fine-grained header control.
PreferRemote conflict resolution for a clean "pull latest from Git" into a
downstream/target workspace.
AVOID
- Reusing a stale
workspaceHead (causes 400 WorkspaceHeadMismatch).
- Passing request bodies via stdin to
fab api (use -i <file.json> or -i '<inline JSON>').
CLI: fab primary, az rest fallback
Every operation below is shown with fab api. Run fab auth login once (it
supports service principals for automation); fab then handles the base URL and
token audience for you. The identical call on the az rest fallback is a
mechanical translation:
Authenticate fab for the identity you are running as:
fab auth login
fab auth login -u <client-id> -p <client-secret> --tenant <tenant-id>
fab auth login --identity
A service principal or managed identity must be workspace Admin to connect or
disconnect (at least Contributor with write on all items is enough for
commit/update) and (for the ConfiguredConnection path) hold the git provider
credentials. See
references/automation-templates.md for full
pipeline scripts.
fab api <endpoint> becomes az rest --method GET --url "https://api.fabric.microsoft.com/v1/<endpoint>" --resource "https://api.fabric.microsoft.com".
fab api -X post <endpoint> -i body.json adds --method POST --headers "Content-Type=application/json" --body @body.json.
fab api reads a request body via -i from either a file path or an inline JSON
string (e.g. -i '{"displayName":"..."}') — never from stdin. It prints
{"status_code": <code>, "text": <parsed body>} (add --show_headers for a
top-level headers object), so read response fields with jq -r '.text.<field>'
and the LRO id with jq -r '.headers["x-ms-operation-id"]'. See
Example 2 for a full az rest pair.
Pre-Flight (before you connect)
Run these Git-relevant checks in order before git/connect. Each failure has a
specific fix — resolving them up front avoids the most common connect-time errors.
| Check | Command | Pass = proceed / Fail = fix |
|---|
| Auth + Fabric token | fab auth login then fab api workspaces | 401 → re-login (fab sets the correct audience automatically) |
| Caller is workspace Admin | fab api "workspaces/${WORKSPACE_ID}/roleAssignments" | Connect/disconnect require the Admin role |
| Workspace has capacity | fab api "workspaces/${WORKSPACE_ID}" → check capacityId | WorkspaceHasNoCapacityAssigned → assign a capacity (Git requires one) |
| Git credential for the SP path | fab api connections (reuse if one already fits) | SP connect needs a ConfiguredConnection. If none exists, the skill creates it from your ADO org/project/repo + SP tenant/client/secret — see Create the Git provider connection (needs the tenant toggle "service principals can create connections"). The interactive-user SSO path uses Automatic and needs no connection. |
Target directoryName exists on the branch | GitHub: curl -fsS -H "Authorization: Bearer $PAT" "https://api.github.com/repos/$OWNER/$REPO/contents/$DIR?ref=$BRANCH" — ADO: Items API on the repo | The API (unlike the portal) does not create a missing folder — connecting to a directoryName that doesn't exist fails with 404 GitProviderResourceNotFound. If the folder is new, pre-create it first (see the note under Connect a Workspace to Git). |
Connect a Workspace to Git
Connect a workspace to an Azure DevOps or GitHub repo, then initialize the
connection to establish the sync direction.
cat > connect.json <<'JSON'
{
"gitProviderDetails": {
"gitProviderType": "AzureDevOps",
"organizationName": "myorg",
"projectName": "MyProject",
"repositoryName": "fabric",
"branchName": "main",
"directoryName": "/workspace-a"
}
}
JSON
fab api -X post "workspaces/${WORKSPACE_ID}/git/connect" -i connect.json
printf '{"initializationStrategy":"PreferWorkspace"}' > init.json
fab api -X post "workspaces/${WORKSPACE_ID}/git/initializeConnection" -i init.json
Use PreferWorkspace when the workspace already has the authoritative items;
PreferRemote when the Git branch is authoritative.
The target directoryName must already exist on the branch. Unlike the Fabric
portal — which silently creates a missing folder by committing a README.md
placeholder into it — the git/connect API does not create the folder.
Connecting to a directoryName that does not exist on the branch fails with
404 GitProviderResourceNotFound. The repo and branch must exist too (connect
never creates a branch). If the folder is new, create it first by committing a
placeholder file to the branch through the Git provider's API (not a Fabric
API), then connect. Match the portal's convention with a README.md whose body is
This is an auto-created file via Fabric skills for <workspace-name>.
DIR="workspace-a"; WS_NAME="My Workspace"
CONTENT=$(printf 'This is an auto-created file via Fabric skills for %s\n' "$WS_NAME" | base64)
cat > mkfolder.json <<JSON
{ "message": "Create ${DIR} folder for Fabric Git connect", "branch": "${BRANCH}", "content": "${CONTENT}" }
JSON
curl -fsS -X PUT -H "Authorization: Bearer ${GITHUB_PAT}" \
"https://api.github.com/repos/${OWNER}/${REPO}/contents/${DIR}/README.md" -d @mkfolder.json
Two authentication modes (both providers use myGitCredentials for automation).
A workspace authenticates to git in one of two ways:
Handle the initialize response. initializeConnection returns
requiredAction (UpdateFromGit, CommitToGit, or None) plus workspaceHead
and remoteCommitHash, and can itself return 202. After it settles, call the
sync operation named by requiredAction with the returned hashes, or the
workspace stays connected but not fully synced. (Learn:
rest/api/fabric/core/git/initialize-connection.)
Create the Git provider connection (service principal)
For unattended automation, first create a Fabric cloud connection that stores the
git provider credentials, then pass its id as the connectionId in the
myGitCredentials block above. Use the
Create Connection API
(POST /v1/connections, delegated scope Connection.ReadWrite.All). A service
principal can create the connection only if the Fabric admin setting "service
principals can create workspaces, connections, and deployment pipelines" is on.
Information you must supply (Azure DevOps):
- ADO organization, project, and repository names, combined into one
url parameter: https://dev.azure.com/<org>/<project>/_git/<repo>/
- The service principal's tenant ID, client (application) ID, and
client secret
- A connection display name (max 200 chars, must be unique in the tenant)
Prerequisites before the connection will work: grant the service principal
(a) access to that Azure DevOps organization and project (git access is
enforced by ADO itself, not only by Fabric), and (b) Admin on the target
Fabric workspace.
trap 'rm -f connection.json' EXIT
umask 077
cat > connection.json <<'JSON'
{
"displayName": "ado-sp-fabric-cicd",
"connectivityType": "ShareableCloud",
"connectionDetails": {
"type": "AzureDevOpsSourceControl",
"creationMethod": "AzureDevOpsSourceControl.Contents",
"parameters": [
{ "dataType": "Text", "name": "url",
"value": "https://dev.azure.com/<org>/<project>/_git/<repo>/" }
]
},
"credentialDetails": {
"credentials": {
"credentialType": "ServicePrincipal",
"tenantId": "<tenant-id>",
"servicePrincipalClientId": "<client-id>",
"servicePrincipalSecret": "<client-secret>"
}
}
}
JSON
fab api -X post "connections" -i connection.json
The 201 response returns the connection id; use it as connectionId in the
myGitCredentials block of the connect call. For a fully secret-free variant,
pass the body inline with -i '{...}' (no file on disk) or reference a Key Vault
secret via servicePrincipalSecretReference instead of the raw
servicePrincipalSecret. The example above uses
credentialType: "ServicePrincipal"; Azure DevOps connections also accept
credentialType: "OAuth2" (a stored delegated user token), and both credential
types support multitenant scenarios. To switch an existing user-connected
workspace to a service principal, add the SP as workspace Admin, share or recreate
this connection, then
PATCH /workspaces/{id}/git/myGitCredentials with the same ConfiguredConnection
body. (Learn:
fabric/cicd/git-integration/automate-git-integration-with-service-principal;
rest/api/fabric/core/connections/create-connection.)
Commit to Git
Push workspace items to the connected branch. Read the head first, then commit.
RESP=$(fab api "workspaces/${WORKSPACE_ID}/git/status" --show_headers)
if [ "$(echo "$RESP" | jq -r '.status_code')" = "202" ]; then
OP=$(echo "$RESP" | jq -r '.headers["x-ms-operation-id"]')
until [ "$(fab api "operations/${OP}" | jq -r '.text.status')" = "Succeeded" ]; do sleep 5; done
RESP=$(fab api "workspaces/${WORKSPACE_ID}/git/status" --show_headers)
fi
HEAD=$(echo "$RESP" | jq -r '.text.workspaceHead')
jq -n --arg head "$HEAD" --arg msg "Author items" \
'{mode:"All", workspaceHead:$head, comment:$msg}' > commit.json
RESP=$(fab api -X post "workspaces/${WORKSPACE_ID}/git/commitToGit" -i commit.json --show_headers)
if [ = ];
OP=$( | jq -r )
[ = ]; 5;
Use "mode": "Selective" with an items array to commit a subset. commitToGit
completes either synchronously (200, already settled) or asynchronously (202
with an x-ms-operation-id to poll) — branch on .status_code as above rather
than assuming 202.
Update from Git
Pull committed items into a workspace (for example, a downstream or target
workspace connected to the same branch/folder).
STATUS=$(fab api "workspaces/${TARGET_WS}/git/status")
TH=$(echo "$STATUS" | jq -r '.text.workspaceHead')
REMOTE=$(echo "$STATUS" | jq -r '.text.remoteCommitHash')
cat > update.json <<JSON
{
"workspaceHead": "${TH}",
"remoteCommitHash": "${REMOTE}",
"conflictResolution": {
"conflictResolutionType": "Workspace",
"conflictResolutionPolicy": "PreferRemote"
},
"options": { "allowOverrideItems": true }
}
JSON
fab api -X post "workspaces/${TARGET_WS}/git/updateFromGit" -i update.json
Check Sync Status
Poll until the workspace and remote heads match. Both commitToGit and
updateFromGit are asynchronous, so this loop is how you know they finished.
for i in $(seq 1 30); do
S=$(fab api "workspaces/${WORKSPACE_ID}/git/status")
WH=$(echo "$S" | jq -r '.text.workspaceHead')
RC=$(echo "$S" | jq -r '.text.remoteCommitHash')
CH=$(echo "$S" | jq -r '.text.changes | length')
[ "$WH" = "$RC" ] && [ "$CH" = "0" ] && { echo "synced at $WH"; break; }
sleep 8
done
For an operation still in flight (a 202 from commit/update), poll the LRO
directly instead of guessing from heads:
OP_ID=$(fab api -X post "workspaces/${WORKSPACE_ID}/git/commitToGit" -i commit.json \
--show_headers | jq -r '.headers["x-ms-operation-id"]')
while :; do
ST=$(fab api "operations/${OP_ID}" | jq -r '.text.status')
case "$ST" in Succeeded) break ;; Failed) echo "op failed"; break ;; *) sleep 5 ;; esac
done
git/status also returns a changes array — a non-empty array means there are
uncommitted (workspace) or unapplied (remote) differences.
Resolve Conflicts
A genuine conflict is changed-on-both-sides: after the last sync, the same
item was modified in the workspace (an uncommitted local edit) and on the
branch (a new commit). git/status flags that item in its changes array with
conflictType: "Conflict" (both workspaceChange and remoteChange show
Modified). This is distinct from a one-sided diff, where only updateFromGit
(remote-only change) or commitToGit (workspace-only change) is needed with no
policy.
When both sides changed an item, updateFromGit requires a conflict policy —
calling it without a conflictResolution block returns
400 MissingWorkspaceConflictResolution:
conflictResolutionPolicy: "PreferRemote" — the Git branch wins (typical for
pull-into-target deployment); the local edit is discarded.
conflictResolutionPolicy: "PreferWorkspace" — the live workspace wins; to
push the workspace version to Git instead use commitToGit, which does not
take a conflict policy.
Set options.allowOverrideItems: true to let the update overwrite items that
already exist in the target workspace (required whenever the conflicting item is
already present in the workspace, which it is by definition in a both-sides
conflict).
Which policy to pick:
| Scenario | Policy |
|---|
| Downstream/target workspace pulls the latest from Git | PreferRemote |
| The authoring workspace is the source of truth | PreferWorkspace (or commitToGit to push the workspace version to Git) |
| Genuine two-sided edits on the same item | Inspect each changes entry's conflictType in git/status and resolve deliberately per item |
Disconnect from Git
fab api -X post "workspaces/${WORKSPACE_ID}/git/disconnect"
Link a Branch Workspace to its Base (Workspace Relations)
Preview. The Git workspace relations APIs are part of the Fabric Git
surface and are in preview. They record a lineage link between a branch
workspace and the base workspace it was branched from, so the base↔branch
relationship is visible in the Fabric portal. Use them when a branch workspace
was created before the built-in branch-out feature shipped, or when branch
workspaces are provisioned by your own automation (not the portal branch-out
UX) and you still want that lineage to show in the UI. Creating a relation does
not connect either workspace to Git, move items, or start a sync — it only
records the link.
Precondition — both workspaces must point at the same Git root. Create fails
with WorkspaceRelationRootDirectoryMismatch unless the base and branch workspaces
are connected to the same repository root, differing only by branch:
- Azure DevOps: same
organizationName, projectName, repositoryName, and
directoryName — only branchName differs.
- GitHub: same repository URL and
directoryName — only branchName differs.
So connect (and initialize) both workspaces to Git first; the relation just makes
the existing branch/base pair visible.
Create a relation (link branch ↔ base)
The relation is bidirectional, so you create it once. Read the body
carefully — the workspace you POST to and the relationType are two different
things:
- The URL workspace (
workspaces/{workspaceId}/...) is the side you call from.
relatedWorkspaceId is the other workspace.
relationType describes the role of that other (relatedWorkspaceId)
workspace, not the workspace in the URL. Only Base and Branch are valid
when creating.
So the same single link has two equivalent ways to create it:
| Call from (URL workspace) | relatedWorkspaceId | relationType |
|---|
| the branch workspace | the base | Base |
| the base workspace | the branch | Branch |
Do not run both — once one direction exists, creating the other fails with
WorkspaceRelationBidirectionalExists. Whichever side you call from, the
permission requirement is the same: Admin on the branch workspace and Contributor
(or higher) on the base workspace. The recommended (and most intuitive) form is
to call from the branch and point at the base:
printf '{"relatedWorkspaceId":"%s","relationType":"Base"}' "${BASE_WS}" > relation.json
fab api -X post "workspaces/${BRANCH_WS}/git/workspaceRelations" -i relation.json
Returns 201. Service principals and managed identities are supported.
List relations
Any Viewer or higher can list a workspace's relations (paginate via
continuationToken). relationType in responses can be Base, Branch, or
RelatedWorkspace.
fab api "workspaces/${WORKSPACE_ID}/git/workspaceRelations"
Delete a relation
Removes the lineage link only — it does not delete either workspace or its
items. The caller must be Admin on the workspace named in the path (either the
base or the branch side). Only a Branch relation is deletable — otherwise the API
returns WorkspaceRelationTypeNotBranch.
fab api -X delete "workspaces/${WORKSPACE_ID}/git/workspaceRelations/${RELATION_ID}"
(Learn: rest/api/fabric/core/workspace-relations.)
Related: cross-workspace item binding
Updating a target workspace from the same branch promotes items into it. Whether
each item-to-item reference rebinds automatically (logical IDs) or breaks (object
IDs / hardcoded endpoints) is a property of the item definition format, not of the
Git lifecycle, and applies equally to fabric-cicd and the Bulk Import API. To
predict a specific dependency's verdict, read the evidence-tagged compatibility
matrix, or empirically validate an uncertain reference, use the
cross-workspace-item-binding skill (a planned companion skill, not yet
available in this repo) — it is a distinct concern from the
connect/commit/update lifecycle this skill drives.
Related: Terraform (infrastructure as code)
To manage the connect side declaratively (infrastructure as code) instead of
with imperative fab api calls, use the git-integration-terraform skill (a
planned companion skill, not yet available in this repo). It
covers the microsoft/fabric provider's fabric_connection and
fabric_workspace_git resources (Azure DevOps service principal or automatic,
GitHub PAT, init strategy, plan/apply/import). In that pattern Terraform owns the
connection, while the commit / update-from-git / status operations in this skill
remain the imperative runtime path — so a common setup is Terraform for the
connection plus a CLI step for update-from-git.
Related: Variable Library (environment parameterization)
Git integration moves item definitions across workspaces, but it does not by
itself parameterize values that must differ per stage (dev/test/prod) — connection
strings, endpoints, capacity IDs, feature flags. That per-environment
parameterization is the job of a Variable Library: define the variables once,
provide a value set per stage, and have items resolve the stage-appropriate value.
When a Git-synced workspace needs values that vary by environment, use the
variable-library-cli companion skill for the variable/value-set
modeling instead of hardcoding or duplicating that logic here — this skill owns
the source-control lifecycle, the Variable Library skill owns the
parameterization it feeds.
Gotchas, Rules, Troubleshooting
| Symptom | Cause | Fix |
|---|
400 WorkspaceHeadMismatch | Stale workspaceHead | Re-read git/status, retry with the current head |
400 MissingWorkspaceConflictResolution | Both sides changed the same item, but updateFromGit was sent without a conflictResolution | Add conflictResolution (policy PreferRemote or PreferWorkspace) + options.allowOverrideItems: true; check git/status for conflictType: "Conflict" first |
WorkspaceHasNoCapacityAssigned | Workspace not on a capacity (Git needs one) | Assign a capacity to the workspace, then retry |
404 GitProviderResourceNotFound on git/connect | The repo, branch, or directoryName does not exist on the provider — the API, unlike the portal, does not auto-create the folder | Verify the repo/branch names; if the folder is new, pre-create it by committing a README.md placeholder to the branch via the Git provider API (see Connect a Workspace to Git), then retry |
WorkspaceAlreadyConnectedToGit | Workspace is already linked to a repo | Disconnect first, then connect to the new repo |
InsufficientPrivileges / 403 | Caller lacks the role the operation needs (connect/disconnect need Admin; commit/update need at least Contributor with write on all items) | Grant the caller the required role: Admin to connect/disconnect, or Contributor with write on all items to commit/update |
PrincipalTypeNotSupported | Service principal using automatic credentials (or GitHub) | Switch to a ConfiguredConnection in myGitCredentials |
commitToGitRequest is required (with fab api) | Body piped via stdin | Write the body to a file, pass |
Examples
Example 1: Commit source, then deploy into a target workspace
SRC_HEAD=$(fab api "workspaces/${SRC_WS}/git/status" | jq -r '.text.workspaceHead')
jq -n --arg head "$SRC_HEAD" --arg msg "Deploy v1" \
'{mode:"All", workspaceHead:$head, comment:$msg}' > commit.json
fab api -X post "workspaces/${SRC_WS}/git/commitToGit" -i commit.json
Example 2: az rest fallback equivalents
When fab is unavailable, the same REST endpoints work through az rest. Always
pass --resource; a body can be inline (--body '{...}') or from a file
(--body @file.json):
az rest --method GET \
--url "https://api.fabric.microsoft.com/v1/workspaces/${WORKSPACE_ID}/git/status" \
--resource "https://api.fabric.microsoft.com"
az rest --method POST \
--url "https://api.fabric.microsoft.com/v1/workspaces/${WORKSPACE_ID}/git/commitToGit" \
--resource "https://api.fabric.microsoft.com" \
--headers "Content-Type=application/json" \
--body "{\"mode\":\"All\",\"workspaceHead\":\"${HEAD}\",\"comment\":\"Author items\"}"