Guide · Pattern 03 · Last reviewed: September 2026

Git and build-driven deployment

The repository is the source of truth, a build step deploys from it into each environment, and the Fabric interface is where you look at the result. You do not promote anything by hand.

This is the pattern for teams who want branches, pull requests, an audit trail, and an approval step the pipeline enforces so nobody has to remember it. It costs a week or more to stand up, and it pays that back the first time somebody needs to undo something.

Azure DevOps or GitHub, holding feature branches and main, reviewed by pull request, then A build step: fabric-cicd or fab deploy, with parameter.yml, then WS_Sales_Dev, holding Git-connected, and WS_Sales_Test, holding Deployed on merge, and WS_Sales_Prod, holding Behind an approval. AZURE DEVOPS OR GITHUBfeature branchesmain, reviewed bypull requestA build step: fabric-cicd or fabdeploy, with parameter.ymlWS_SALES_DEVGit-connectedWS_SALES_TESTDeployed on mergeWS_SALES_PRODBehind an approvalData NovaSave SVG

Who it fits

  • Every environment on Fabric capacity
  • More than two people building at once
  • A real approval requirement, or an auditor who will ask
  • Someone on the team comfortable with Azure DevOps or GitHub Actions and Python

Git and the build step do different jobs

Git integration connects a workspace to a branch and syncs item definitions. The build step deploys from the repository into a workspace. Most deployment trouble starts with expecting git integration to do the build step's job.

WS_Sales_Dev, holding Where you build, then Git integration syncs, both ways, then The repository, holding Item definitions as files and Branches, review, history, then A build step deploys. Git never does this, then The other environments, holding WS_Sales_Test and WS_Sales_Prod. WS_SALES_DEVWhere you buildGit integration syncs, both waysTHE REPOSITORYItem definitionsas filesBranches, review,historyA build step deploys. Git never doesthisTHE OTHER ENVIRONMENTSWS_Sales_TestWS_Sales_ProdData NovaSave SVG
A Fabric workspace with a Git status column reading Synced against every item, and the Source control pane open showing the current branch as main with no pending changes.
The Fabric side. A Git status column appears once a workspace is connected, and the Source control pane is where you see what is in step with the branch and what is not.
A Fabric workspace with a Git status column reading Synced against every item, and the Source control pane open showing the current branch as main with no pending changes.
The same workspace in an Azure DevOps repository, showing one folder per item named for the item and its type: LH_ZZ_Dev.Lakehouse, four .Notebook folders, RPT_ZZ_DeployTest.Report, two .SemanticModel folders, and a README.
The repository side, after one commit. One folder per item, each named for the item and its type, which is the shape a build step reads.
The same workspace in an Azure DevOps repository, showing one folder per item named for the item and its type: LH_ZZ_Dev.Lakehouse, four .Notebook folders, RPT_ZZ_DeployTest.Report, two .SemanticModel folders, and a README.
Update-from-git is not a deployment. It brings everything in at once, in name order rather than dependency order, with no data, no connections and no credentials. If pipeline A invokes pipeline B, the activity stores a workspace ID and B's own item ID. Within one workspace those rebind; across workspaces they do not, and the reference can be left pointing at the pipeline you deployed from instead of failing outright.

The fix is to make the name carry the order: prefix co-dependent items with a number, 1_, 2_, so the sync reaches them in the sequence they need. It is blunt, and it sits outside the pattern in the naming guide, which has no slot for a sequence number. Use it only where a dependency actually bites.

Practitioners worked this one out and it is not in any documentation, so treat it as a field fix. The fix is its own evidence: renaming an item would change nothing unless the sync order followed the name. Microsoft documents the symptom, in a page about resolving dependency errors on sync.

Branching

BranchConnected toWho writesDeploys to
feature/<name>A per-developer workspace, optionalOne developerNothing automatically
mainWS_Sales_DevNobody directly, pull request onlyDev on merge, then Test
A tag or release branchNothingThe release managerProd, behind an approval gate
Dev is the only git-connected workspace. Connecting every stage to its own branch is documented, but it removes the build step, and with it any chance of substituting a value per environment.

Something always has to differ between environments, so you need a build step either way. That is why a branch per stage looks like the tidy answer and is the one to avoid.

Two developers, working at the same time, which is the reason you are on this pattern at all.

Developer A, holding feature branch, own workspace, and Developer B, holding feature branch, own workspace, then merge, then Pull request into main, holding the other one reviews it, then the build carries it on, then WS_Sales_Dev, holding the build writes here, nobody by hand, then only after the gate, then WS_Sales_Test, holding the same build, same commit, then Approval, holding somebody has to say yes, then WS_Sales_Prod, holding nothing reaches here unapproved. Developer Afeature branch, own workspaceDeveloper Bfeature branch, own workspacePull request into mainthe other one reviews itWS_Sales_Devthe build writes here, nobody by handWS_Sales_Testthe same build, same commitApprovalsomebody has to say yesWS_Sales_Prodnothing reaches here unapprovedmergethe build carries it ononly after the gatestarts and endsthe path betweensomebody decidesData NovaSave SVG

The deploy tool

fabric-cicd(opens in a new tab), published by Microsoft, at v1.3.0 released 10 August 2026, running on Python 3.9 to 3.13. Its documentation lists the item types it supports, and that count is on a documentation page rather than in the package, so I have not confirmed it. It publishes in dependency order and rebinds logical IDs to the target workspace, which is the thing a raw git sync does not do. Since Fabric CLI v1.5 there is a single wrapping command, fab deploy.

Its documented limitations are worth knowing before you design around it:

  • Full deployment every time. It does not look at commit diffs, so every run deploys everything in scope.
  • It deploys into the tenant of whichever identity runs it. You cannot deploy across tenants.
  • Only items that have source control and public create or update APIs are supported.

Per-environment values live in a reviewed file

parameter.yml holds find-and-replace values keyed by environment, and the environment argument you pass must match a key in it. Keep it in the repository next to the items, reviewed like everything else. It does the same job as a deployment rule, but in a file instead of a portal, so you can diff it, review it and see who changed it.

find_replace:
  - find_value: "sql-dev.database.windows.net"
    replace_value:
      DEV:  "sql-dev.database.windows.net"
      TEST: "sql-test.database.windows.net"
      PROD: "sql-prod.database.windows.net"

The build definition is two stages with an approval between them. The approval is attached to the Azure DevOps environment, not to the script, so it cannot be skipped by editing the script.

# Azure DevOps, a sketch. Read it before running: this deploys into a live workspace.
trigger:
  branches: { include: [main] }

stages:
  - stage: DeployTest
    jobs:
      - job: Deploy
        steps:
          - script: pip install fabric-cicd==1.3.0
          - script: python deploy.py --environment TEST

  - stage: DeployProd
    dependsOn: DeployTest
    jobs:
      - deployment: Deploy
        environment: 'fabric-prod'   # the approval gate lives on this environment
        strategy:
          runOnce:
            deploy:
              steps:
                - script: pip install fabric-cicd==1.3.0
                - script: python deploy.py --environment PROD

Identity, and how the two git providers differ

The build agent authenticates with a TokenCredential, which in practice is a service principal. It needs workspace contributor or above on every target workspace, and it needs the tenant setting "Service principals can call Fabric public APIs" enabled.

That tenant setting is an admin switch, and in a client tenant it belongs to the client's admin. Raise it at design time. Discovering it at cutover is a bad day.

You connect the workspace to the repository in workspace settings, once. The branch has to exist before you point at it, and a workspace holds one connection at a time, so disconnect it before repointing it somewhere else.

Fabric workspace settings on the Git integration tab, showing provider Azure DevOps with an Entra account, the organisation, project and repository, a Git folder of a single forward slash, the branch set to main, and a Disconnect workspace button.
Workspace settings, Git integration, once those calls have run. Git folder here is directoryName in the connect call, and a single slash is what it looks like when it worked.
Fabric workspace settings on the Git integration tab, showing provider Azure DevOps with an Entra account, the organisation, project and repository, a Git folder of a single forward slash, the branch set to main, and a Disconnect workspace button.

Azure DevOps uses the signed-in identity and needs no token entered in the Fabric interface. GitHub requires a personal access token pasted into Fabric. Commit size limits differ too. Microsoft documents 25 MB through a service principal on Azure DevOps and 125 MB through the default single sign-on Entra ID account, GitHub 50 MB, and a 1,000 item cap on the workspace. Those are their figures: my rig committed 33 files whose largest was empty, so nothing I ran goes anywhere near them.

Expect noise on the first commit that you did not write. The service rewrites TMDL when it imports it, so your first diff carries reordering and whitespace changes on top of your actual change. It settles after that.

What it costs

Capacity on every environment, as with deployment pipelines. Plus build minutes, which are negligible. The engineering time to build and maintain it is not.

What breaks

  • Nothing carries credentials. Not git, not fabric-cicd, not fab deploy. Somebody has to bind the connections in each target environment. Decide who, before you find out the hard way.
  • Full deploys only means a change to one report redeploys everything in scope.
  • Reports and semantic models in git integration are still preview, so do not design on the assumption that every item type is equally well supported. Preview describes the support you get, not how well it works. I committed a lakehouse, four notebooks, a PBIR report and two semantic models, and every one of them round-tripped: 33 files across 21 folders. The lakehouse wrote four metadata files and no tables, which is what it looks like on disk when the item travels and the data stays where it is.
  • A commit with nothing to commit answers 400. If the workspace and the branch are already in step, commitToGit returnsNoChangesToCommit, "There are no changes to commit." A script that treats any 400 as a failure will stop there, on a run where nothing was wrong.
  • Circular references fail outright: a two-way dependency between two items will stop a deploy.
  • The service principal tenant switch is somebody else's decision in a client tenant.
  • Connecting fails with a misleading error if the directory does not exist. Point directoryName at a folder that is not in the repository yet and the connect call returns GitProviderResourceNotFound, which reads as a missing repository and sends you checking the organisation and project names instead. "/" connects, and Fabric builds the layout underneath it.

When this breaks down

Rarely, once every environment sits on capacity. What comes after is scale work: more repositories, shared component libraries, a workspace per developer provisioned by automation. The pattern stays the same, there is just more of it.

Everything still applies on what is true in every pattern, starting with the credential step that none of these tools does for you.