Learn how to combine Microsoft Fabric's Git integration and deployment pipelines into a production-grade SDLC. This deep-dive lesson covers repository serialization, branching strategies, deployment rules, CI/CD automation, and the edge cases that trip up even experienced Fabric engineers.

Picture this: your team has spent three months building a sophisticated analytics platform in Microsoft Fabric. There's a lakehouse with carefully engineered medallion layers, a set of Spark notebooks transforming raw data into business-ready gold tables, Dataflow Gen2 definitions pulling from a dozen sources, and a suite of Power BI reports your executive team relies on every Monday morning. Everything is in production and working beautifully — until someone makes a "quick fix" directly in the production workspace, introduces a bug in a notebook's PySpark logic, and now the gold layer is silently producing incorrect aggregations. You have no record of what changed, no easy way to revert, and no test environment to validate the fix before it goes live again.
This is the problem that Fabric Git integration and deployment pipelines were built to solve. Together, they give you a professional-grade software development lifecycle (SDLC) for your Fabric artifacts: version control for tracking and reverting changes, branching workflows for parallel development, and structured promotion paths for moving changes from development through testing to production with auditability and control. By the end of this lesson, you'll be able to connect a Fabric workspace to a Git repository, understand exactly what gets serialized to disk and why, design a branching strategy appropriate for your team, and configure deployment pipelines that move validated artifacts across environments while managing the configuration differences between them.
What you'll learn:
You should be comfortable with:
You'll need:
Before diving into configuration, it's worth being precise about what these two features actually are and how they relate to each other, because the marketing language often blurs the distinction.
Fabric Git integration is a bidirectional sync between a Fabric workspace and a branch in a Git repository. It handles version control: tracking who changed what, when, and why. It enables branching, code review via pull requests, and rollback through Git history. Think of it as the "source of truth" layer.
Deployment pipelines are a promotion mechanism: a structured workflow for moving artifacts from one workspace to another (typically Dev → Test → Prod). They handle the environmental differences between stages — connection strings that point to different databases, parameters that differ between environments, and so on. Think of them as the "change management" layer.
These two systems are complementary and somewhat independent. You can use Git integration without deployment pipelines (relying on separate Git branches per environment instead). You can use deployment pipelines without Git integration (though you lose version control). Most mature teams use both together, which we'll cover. But understanding that they solve different problems is critical to designing a workflow that doesn't fight itself.
Key insight
Deployment pipelines move artifacts between workspaces. Git integration syncs artifacts between a workspace and a repository branch. They operate on different axes, which is why you need both to build a complete SDLC.
When you connect a workspace to Git and commit, Fabric serializes each supported item into a folder structure in your repository. Understanding this serialization is essential — it determines what you can meaningfully review in a pull request and what limitations you'll encounter.
The repository structure follows this pattern:
/
├── .platform/
│ └── config.json
├── SalesLakehouse.Lakehouse/
│ ├── .platform
│ └── lakehouse.metadata.json
├── TransformSilverLayer.Notebook/
│ ├── .platform
│ └── notebook-content.py
├── IngestRawData.DataPipeline/
│ ├── .platform
│ └── pipeline-content.json
├── SalesDataflow.DataflowsV2/
│ ├── .platform
│ └── (mashup definition files)
└── SalesSemanticModel.SemanticModel/
├── .platform
├── definition.pbism
├── model.bim
└── (TMDL folder structure)
Each item folder contains a .platform file that stores item metadata (the item's logical ID, type, and display name), plus one or more content files whose format depends on the item type.
Notebooks are serialized as .py (PySpark), .r, or .scala files — actual source code that diffs cleanly in Git. This is one of the most valuable aspects: your PySpark transformation logic is version-controlled as readable Python.
Data pipelines are serialized as JSON, representing the pipeline's activity graph, linked service references, and parameters. These diffs can be verbose but are fully reviewable.
Semantic models use the Tabular Model Definition Language (TMDL) format — a text-based, human-readable representation of the model's tables, measures, relationships, and data sources. TMDL is a significant improvement over the binary .pbix format precisely because it makes model changes reviewable.
Lakehouses and warehouses themselves are metadata-only in Git. The lakehouse definition (its name, schema, properties) is stored, but the data inside it is not. This is correct behavior — you're versioning the structure, not the data. Delta tables live in OneLake, not in Git.
Warning
Reports (.Report items) are Git-integrated, but they store a JSON definition. If your team uses the Fabric web editor to modify reports directly, those changes must be explicitly committed. Uncommitted report changes sitting in a workspace with no Git backing are a silent risk — they can be overwritten by the next Git sync without warning.
As of mid-2025, several item types have partial or no Git integration:
This matters for your architectural decisions. If you're building real-time analytics with eventstreams, as described in Real-Time Analytics in Microsoft Fabric, understand that not all those artifacts will be in Git with the same fidelity as notebooks and pipelines.
Once connected, the workspace and the repository branch exist in one of three states at any given moment:
The Fabric UI shows this status in the workspace's Git panel, with a diff count per item. Committing is explicit — Fabric doesn't auto-commit. This means a developer can iterate freely in the Fabric UI, then commit when they're satisfied. However, it also means the workspace can drift from the repository if discipline lapses.
Tip
Adopt a team norm that the Dev workspace is always synced to the main branch. Developers work on personal feature branches synced to their own sandboxes (or use the branch-per-developer model we'll discuss), and commits go through pull requests. If the Dev workspace lives directly on main with uncommitted local changes, you've lost the point of version control.
In the Fabric portal, navigate to your workspace settings (the gear icon in the workspace header). Select Git integration from the left nav. You'll see a connection form asking for:
/, but /fabric or /items is cleaner if the repo also contains other code)After connecting, Fabric shows you a comparison between what's in the workspace and what's in the repository. For a fresh connection to an existing workspace, all items will show as "Only in workspace" — you'll need to do an initial commit to push them to Git.
For GitHub connections, the process is similar but requires OAuth authorization. GitHub connections currently require a personal access token or GitHub App — service principal auth is more mature on the Azure DevOps path, which matters for CI/CD automation.
When you commit, you can select individual items or commit all changes. The commit UI presents a checkbox list of changed items with their change type (Added, Modified, Deleted). You write a commit message directly in the Fabric UI.
This is serviceable for individual developers but becomes limiting for teams. The better pattern is to use the Fabric REST API or the fabric-cli tool to automate commits from a CI pipeline, where you have full control over commit messages, authorship, and triggering conditions.
# Example: using the Fabric REST API to trigger a Git commit
# POST https://api.fabric.microsoft.com/v1/workspaces/{workspaceId}/git/commitToGit
curl -X POST \
"https://api.fabric.microsoft.com/v1/workspaces/$WORKSPACE_ID/git/commitToGit" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mode": "All",
"comment": "feat: update silver layer notebook to handle null CustomerID",
"workspaceHead": {
"id": "'$WORKSPACE_HEAD_ID'"
}
}'
The workspaceHead value is a hash representing the current state of the workspace — you retrieve it from the GET /git/status endpoint first to avoid committing over concurrent changes.
Note
The workspaceHead mechanism is Fabric's concurrency control. If two processes try to commit simultaneously using different workspace head IDs, the second one fails. This is intentional — it prevents commit collisions in shared workspaces. Design your CI automation to handle this gracefully with retry logic.
Standard Git branching strategies (GitFlow, trunk-based development, GitHub Flow) all apply to Fabric, but they need to account for one Fabric-specific constraint: each workspace tracks one branch at a time. You can't check out a feature branch in an existing workspace — you'd need to connect a new workspace to that branch, or disconnect and reconnect.
For most professional teams, the most practical model is:
Branches: Workspaces:
main → Prod workspace
staging → Test workspace
develop → Dev workspace
feature/xyz → Developer's personal sandbox workspace (ephemeral)
Developers create feature branches off develop. They connect a personal sandbox workspace to their feature branch, do their work, commit to Git from there, then open a pull request into develop. After code review, the PR is merged, and the Dev workspace (tracking develop) is updated via Git pull. Promotion from Dev to Test happens either via another PR (develop → staging) or via deployment pipeline.
This model has the highest isolation — each environment has its own data, its own connections, and its own workspace — but it also has the highest infrastructure cost: you need enough F SKU capacity to run all these workspaces concurrently.
A leaner alternative, especially for smaller teams:
Branches: Workspaces:
main (single source of truth)
feature/xyz → Developer's personal sandbox (ephemeral or shared)
Deployment pipeline: Dev → Test → Prod (all tracking main)
In this model, developers work on short-lived feature branches. Merging to main automatically triggers (or the team manually triggers) a sync to the Dev workspace, then the team uses the deployment pipeline to promote to Test and Prod. There's no staging or develop branch — just main, feature branches, and the pipeline for promotion.
The trade-off: if multiple features are in flight simultaneously, they're competing to merge into main, and the deployment pipeline becomes a sequential bottleneck. This works well for teams where features are small and release frequency is high (multiple times a week), but breaks down for teams with long-running feature development.
Key insight
The branching strategy you choose has direct cost implications. Ephemeral sandbox workspaces that spin up for feature development and are deleted after merge don't need persistent capacity, but they do need automation to provision and deprovision them. Budget for that automation time when choosing a strategy.
Merge conflicts in Fabric Git repos are a real operational challenge, and they behave differently across item types.
Notebook conflicts are the most tractable. Since notebooks are serialized as .py files, they get standard line-level Git conflicts. If developer A modifies the top of a PySpark notebook while developer B modifies the bottom, Git can auto-merge. Conflicts in the same area require manual resolution in a text editor — and importantly, the resolved file is valid Python that Fabric will accept on re-import.
Pipeline conflicts are more painful. The JSON representation of a data pipeline is deeply nested and often has large blocks of boilerplate. Two developers editing different activities in the same pipeline will produce conflicts in the JSON that require careful manual resolution to preserve validity. The practical mitigation is to keep pipelines small and composable — a pipeline that calls a notebook isn't in conflict with the notebook itself.
Semantic model conflicts (TMDL format) fall between these extremes. TMDL is human-readable and line-oriented, so simple changes (adding a measure, modifying a column description) have clean diffs. But changes to relationships or table structures can produce conflicts that require understanding the model's semantics to resolve correctly. If two developers add measures to the same table simultaneously, the merge is usually clean. If both modify the same measure's DAX, you need to reason about which version is correct — there's no automated path.
The general principle: prevent conflicts through work decomposition rather than relying on merge tooling. Assign ownership of semantic model areas, and coordinate changes to shared pipeline orchestrations.
In the Fabric portal, navigate to Deployment pipelines from the left rail (the icon looks like a branching flow diagram). Create a new pipeline and give it a name like Sales Analytics Platform. Add stages: Development, Test, Production. Each stage gets assigned a workspace.
The pipeline stages are connected linearly — you can only deploy forward (Dev → Test → Test → Prod). There's no built-in concept of promoting directly from Dev to Prod, skipping Test. This is a guardrail, not a limitation.
Once workspaces are assigned, the pipeline shows a side-by-side comparison of items across stages. Items are compared by their logical ID (stored in the .platform metadata file), not by name. This is important: if you manually create an item in Test with the same name as one in Dev, the pipeline won't consider them the same item unless they share a logical ID. Always let the pipeline create items in downstream stages rather than manually recreating them.
Deploying is explicit. You click Deploy between stages, select which items to deploy (all or specific items), optionally set a deployment note, and confirm. The pipeline copies the item definitions from the source stage into the target stage workspace.
Warning
Deploying a semantic model or report to production overwrites the existing version. There is no automatic backup before deployment. Your Git history is your backup. This is another reason why Git integration isn't optional — it's your only rollback path if a deployment causes problems.
This is where deployment pipelines become genuinely powerful — and where teams most commonly misconfigure things.
A deployment rule overrides a specific property of an item when it lands in a target stage. The canonical use case: a Dataflow Gen2 that connects to an Azure SQL database should point to the dev database in the Dev workspace, the test database in the Test workspace, and the prod database in the Prod workspace. Without deployment rules, deploying the dataflow would carry the dev connection string into production.
To configure rules, click the wrench icon on an item in the pipeline view. Rules can override:
mssparkutilsFor a data pipeline that takes a parameter for the target lakehouse connection, you'd set a rule like:
Stage: Production
Item: IngestRawData (Data Pipeline)
Parameter: TargetConnectionId
Override value: <prod-connection-guid>
For a Dataflow Gen2 that connects to an Azure SQL source, the rule overrides the connection reference at the data source level.
Deployment rules have real gaps that you need to plan around:
Notebooks don't support connection overrides via rules in the traditional sense. A PySpark notebook that hardcodes a connection string or lakehouse path will carry those values into production. The correct pattern is to parameterize notebooks using mssparkutils.runtime.context or by reading configuration from a known location (a config Delta table, a mounted key vault secret, or a pipeline parameter passed at execution time).
Here's a notebook pattern that reads environment-specific config cleanly:
# Determine current workspace context
workspace_id = mssparkutils.runtime.context['currentWorkspaceId']
# Load environment config from a well-known Delta table
config_df = spark.read.format("delta").load(
f"abfss://config@onelake.dfs.core.windows.net/{workspace_id}/Tables/env_config"
)
config = {row['key']: row['value'] for row in config_df.collect()}
silver_lakehouse = config['silver_lakehouse_path']
raw_lakehouse = config['raw_lakehouse_path']
# Now use config values instead of hardcoded paths
df = spark.read.format("delta").load(raw_lakehouse + "/Tables/raw_orders")
This pattern makes the notebook genuinely environment-agnostic. Each workspace has its own env_config table with stage-specific values, and the notebook discovers its environment at runtime.
Semantic models reference their data source via a connection that must be overridden via a rule. If your semantic model uses Direct Lake mode over a lakehouse (as described in Direct Lake Mode in Power BI), the lakehouse SQL endpoint reference must be updated to point to the production lakehouse. Without this rule, your production Power BI reports will be reading from the dev lakehouse — an insidious, silent data quality problem.
Tip
Before your first production deployment, create a checklist of every item with an external connection reference and verify each has a corresponding deployment rule. The pipeline will not warn you if a rule is missing — it will simply carry the source-stage value forward.
Now let's put both systems together into a workflow that a real team can operate.
1. Developer creates feature branch off develop
2. Developer connects personal sandbox workspace to feature branch
3. Development work happens in sandbox: edits notebooks, pipelines, dataflows
4. Developer commits changes from sandbox to feature branch
5. Developer opens PR: feature/xyz → develop
6. PR is reviewed, CI checks run (optional but recommended)
7. PR is merged into develop
8. Dev workspace (tracking develop) syncs: team pulls latest from Git
9. Integration testing happens in Dev workspace
10. When ready, PR: develop → main is opened and merged
11. Test workspace (tracking main) syncs and pulls
12. Deployment pipeline: Dev stage → Test stage deploy (or Git-driven)
13. QA testing in Test workspace
14. Deployment pipeline: Test stage → Prod stage deploy
15. Production monitoring confirms health
The key design decision is step 12: do you promote from Dev to Test via the deployment pipeline, or via Git? There are two schools of thought:
Git-driven promotion: The Test workspace tracks the main branch. When develop is merged to main, the Test workspace syncs. The deployment pipeline is only used for Test → Prod. This keeps Git as the single source of truth across all environments.
Pipeline-driven promotion: Dev, Test, and Prod workspaces all participate in the deployment pipeline. Git manages Dev workspace. The pipeline propagates from Dev to Test to Prod. This gives you more control over the timing of promotions (you can choose when to push to Test, independent of Git merges).
Neither is universally superior. The Git-driven approach is simpler to reason about but means Test always reflects main immediately after merge (no "hold" option). The pipeline-driven approach decouples promotion timing from merge timing but introduces a potential drift: the pipeline might be carrying a state that's slightly behind what Git shows.
You can automate the workspace Git sync using the Fabric REST API from an Azure DevOps YAML pipeline. Here's a realistic ADO pipeline definition that triggers when a PR merges to main and syncs the Test workspace:
# azure-pipelines.yml
trigger:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
FABRIC_WORKSPACE_ID: $(fabricTestWorkspaceId) # stored in ADO variable group
FABRIC_TENANT_ID: $(tenantId)
FABRIC_CLIENT_ID: $(servicePrincipalClientId)
FABRIC_CLIENT_SECRET: $(servicePrincipalSecret)
steps:
- task: Bash@3
displayName: 'Acquire Fabric Token'
inputs:
targetType: 'inline'
script: |
TOKEN=$(curl -s -X POST \
"https://login.microsoftonline.com/$FABRIC_TENANT_ID/oauth2/v2.0/token" \
-d "grant_type=client_credentials" \
-d "client_id=$FABRIC_CLIENT_ID" \
-d "client_secret=$FABRIC_CLIENT_SECRET" \
-d "scope=https://api.fabric.microsoft.com/.default" \
| jq -r '.access_token')
echo "##vso[task.setvariable variable=ACCESS_TOKEN;issecret=true]$TOKEN"
- task: Bash@3
displayName: 'Get Current Workspace Git Status'
inputs:
targetType: 'inline'
script: |
STATUS=$(curl -s \
"https://api.fabric.microsoft.com/v1/workspaces/$FABRIC_WORKSPACE_ID/git/status" \
-H "Authorization: Bearer $ACCESS_TOKEN")
REMOTE_COMMIT=$(echo $STATUS | jq -r '.remoteCommitHash')
echo "##vso[task.setvariable variable=REMOTE_COMMIT]$REMOTE_COMMIT"
echo "Remote commit to sync: $REMOTE_COMMIT"
- task: Bash@3
displayName: 'Update Workspace from Git'
inputs:
targetType: 'inline'
script: |
RESULT=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
"https://api.fabric.microsoft.com/v1/workspaces/$FABRIC_WORKSPACE_ID/git/updateFromGit" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"remoteCommitHash": "'"$REMOTE_COMMIT"'",
"conflictResolution": {
"conflictResolutionType": "Workspace",
"conflictResolutionPolicy": "PreferRemote"
},
"options": {
"allowOverrideItems": true
}
}')
if [ "$RESULT" -ne "200" ] && [ "$RESULT" -ne "202" ]; then
echo "Git sync failed with HTTP $RESULT"
exit 1
fi
echo "Git sync initiated successfully"
A few important details in this script:
The conflictResolutionPolicy: PreferRemote setting tells Fabric that if there are conflicts between what's in the workspace and what's in the repository, the repository wins. For an automated CI pipeline syncing a shared Test workspace, this is almost always correct — the repository is the source of truth, and any local workspace changes that aren't committed are aberrations.
The allowOverrideItems: true flag is necessary when items in the workspace have been modified outside of Git (through direct UI changes). Without it, the sync will fail if it detects workspace-ahead changes.
Service principal authentication is strongly preferred over personal access tokens for automation. You'll need to register an Azure AD app, grant it the Fabric workspace contributor role, and enable service principal access at the tenant level in Fabric Admin settings.
Warning
The updateFromGit API call is asynchronous when the operation takes more than a few seconds — it returns 202 with an operation ID rather than 200. A robust implementation polls the operation status endpoint until it reaches "Succeeded" or "Failed." The simple script above doesn't handle this; production automation should.
If your architecture spans multiple workspaces — for example, a shared Fabric Lakehouse in one workspace referenced by notebooks in another — Git integration and deployment pipelines become more complex.
Workspace IDs are environment-specific. A notebook that references abfss://gold@onelake.dfs.core.windows.net/<workspace-id>/ with a hardcoded workspace ID will break when deployed to a different workspace. The deployment pipeline has a workspace override rule to handle this, but you must explicitly configure it for every cross-workspace reference.
The cleaner long-term pattern is to use OneLake shortcuts to abstract cross-workspace references. A shortcut in the Dev lakehouse pointing to the shared bronze data has its own logical identity — and when deployed to Prod, the deployment rule overrides the shortcut's target to the corresponding production source. This is cleaner than updating workspace IDs in notebook code.
When you version a Fabric Warehouse, Git stores the warehouse metadata but not the schema DDL as runnable SQL scripts. This is a meaningful gap: if you add a column to a table in the Dev warehouse and deploy via the deployment pipeline, Fabric will attempt to update the target warehouse's schema — but if the target has existing data, column additions usually work while column deletions or type changes may fail.
The recommended pattern for schema changes is to manage DDL explicitly in your CI pipeline:
-- migrations/0042_add_customer_tier_column.sql
-- Run this against the target warehouse connection before deploying
IF NOT EXISTS (
SELECT 1 FROM sys.columns
WHERE object_id = OBJECT_ID('gold.dim_customer')
AND name = 'customer_tier'
)
BEGIN
ALTER TABLE gold.dim_customer
ADD customer_tier VARCHAR(20) NULL;
END
Store these migration scripts in Git alongside your Fabric items, and run them as a pre-deployment step in your ADO pipeline against the target environment. This gives you the same schema migration discipline that tools like Flyway or Liquibase provide for operational databases.
Items deployed by the pipeline to a downstream stage have their own logical ID in that stage's workspace. If you delete an item in the Dev workspace and redeploy, the pipeline will show the deletion as a difference — but it will not automatically delete the corresponding item in Test or Prod. You must explicitly confirm deletions during deployment.
This is intentional — the pipeline is conservative about destructive operations — but it means your Prod workspace can accumulate orphaned items (old pipelines, deprecated notebooks) that are no longer in Dev. Periodically audit all three stages for items that are in Prod but not in Dev, and explicitly remove them.
Personal workspaces (the "My Workspace" in Fabric) do not support Git integration or deployment pipeline stages. This is a deliberate design choice — Fabric's governance model assumes team collaboration happens in shared workspaces. Developers who want to do preliminary exploration can use personal workspaces, but anything that needs version control must be moved to a shared workspace first.
Note
This limitation has an important implication for onboarding. New team members who've been working in personal workspaces may have significant work that's never been committed to Git. Build an onboarding checklist that includes migrating personal workspace items into a shared sandbox workspace and connecting it to a feature branch before any "real" work begins.
This exercise walks you through setting up a minimal Git-integrated, pipeline-promoted Fabric environment for a sales analytics scenario.
Scenario: You have a sales analytics workspace with a lakehouse (containing bronze and silver Delta tables), two Spark notebooks (one for bronze-to-silver transformation, one for silver-to-gold), and a semantic model with a Direct Lake connection.
Part 1: Connect Dev Workspace to Git
develop and the root folder to /fabric.chore: initial commit of sales analytics platform.Part 2: Make a Change via Feature Branch
feature/add-customer-tier-logic off develop.feature/add-customer-tier-logic branch, and pull from Git to populate it.from pyspark.sql.functions import when, col
df_gold = df_silver.withColumn(
"customer_tier",
when(col("annual_spend") >= 50000, "Platinum")
.when(col("annual_spend") >= 20000, "Gold")
.when(col("annual_spend") >= 5000, "Silver")
.otherwise("Standard")
)
df_gold.write.format("delta").mode("overwrite").option("overwriteSchema", "true") \
.save(f"{gold_lakehouse_path}/Tables/dim_customer")
feat: add customer tier classification to dim_customer.feature/add-customer-tier-logic → develop in ADO. Review the diff — confirm the notebook change is the only modification.Part 3: Sync Dev Workspace and Verify
customer_tier column populates correctly.Part 4: Configure and Execute Deployment Pipeline
Validation: Your production workspace's semantic model should be reading from the production lakehouse, and the dim_customer table should now include the customer_tier column populated according to your tier logic.
Almost always caused by having workspace-level caching or by pulling into the wrong workspace. Verify which branch the workspace is connected to in Git settings. If you merged a PR and then pulled in the workspace, confirm the branch matches.
Rules apply only to outgoing deployments from the stage where they're configured. A rule on the Test stage governs what happens when you deploy from Test to Prod, not what comes into Test. This confuses nearly everyone the first time. If you want the connection to be overridden when something arrives in Test, configure the rule on the Dev stage for the Test deployment, or configure it on the Test stage for the Prod deployment — and make sure you're looking at the correct direction.
Someone made changes directly in the Dev workspace that weren't committed to Git, and now the repo and workspace disagree. You have two choices: commit the workspace changes (if they're intentional) before merging, or use the PreferRemote conflict resolution policy to discard the workspace-local changes. If you can't determine which is the right call, check the Git log and the Fabric audit log (in the admin portal) to see what changed and when.
Two common root causes: (1) Service principals aren't enabled in Fabric tenant settings — an admin must enable "Service principals can use Fabric APIs" in the Admin portal. (2) The service principal isn't assigned to the workspace with at minimum Contributor role. Member or Admin is needed for Git operations.
The Direct Lake connection wasn't overridden by a deployment rule. Open the pipeline, click the wrench on the semantic model item, and verify a rule exists for the target stage. If the rule exists but isn't taking effect, check that the data source name in the TMDL exactly matches what the rule is targeting — naming mismatches cause silent rule failures.
The service principal or user identity running the notebook in Prod doesn't have access to the Prod lakehouse. Fabric uses the calling identity's permissions, not the workspace's permissions. Grant the executing identity Contributor on the Prod workspace, or use mssparkutils.credentials with a managed identity configured for the Prod environment.
You now have a complete picture of how Fabric Git integration and deployment pipelines work together to create a professional SDLC for your analytics platform. Let's consolidate the key design principles:
Git integration is your source of truth. Every artifact that can be version-controlled should be. Commit discipline — through either manual conventions or automated CI — is what separates a team that can recover from incidents quickly from one that rebuilds from memory.
Deployment pipelines are your change management layer. They're not a substitute for Git; they solve the distinct problem of how to move artifacts between environments while adapting to environmental differences via rules.
Parameterize everything. Whether it's notebook paths, pipeline connection IDs, or semantic model data sources, any value that differs between Dev, Test, and Prod must be externalized. Hardcoded values are technical debt that causes deployment failures.
Design your branching strategy around your team's rhythm. Three branches with three persistent workspaces is right for teams with long-lived features and strict environment separation. Trunk-based with deployment pipelines is right for teams that ship frequently and can tolerate the coupling between merge timing and environment promotion.
Automate the mechanical parts. Git syncs, deployment triggers, and health checks after deployment should all be automated via the Fabric REST API and your CI/CD tool of choice. Manual clicks in the deployment pipeline UI are appropriate for initial setup and occasional manual overrides, not for a production release process.
From here, the logical next topics to explore are:
The investment in setting up this infrastructure pays off quickly. The first time you ship a bad notebook change, catch it in Test instead of Prod, and roll it back with a single Git revert, you'll understand exactly why this work is worth doing.