Fabric CI/CD Is Solved. Your Tenant Isn't. — Treating a Microsoft Fabric Tenant as Declarative, Source-Controlled Infrastructure
Shipping Fabric items — notebooks, pipelines, semantic models — through Git and a CI runner is a settled problem. Microsoft documents four workflow patterns, and fabric-cicd has emerged as the de-facto deployment tool. The unsolved problem is upstream and around it: what is the source of truth for the tenant itself, and how is platform governance — workspaces, capacity, domains, tenant settings, connections, roles — expressed as reviewable, source-controlled state. Git integration does not manage that layer. This article treats the Fabric tenant as declarative infrastructure and shows where the item pipeline ends and tenant-as-code begins.
The four Fabric CI/CD workflow options
Microsoft’s CI/CD workflow options article defines exactly four patterns, with the explicit caveat that real deployments often combine them into hybrids. The differentiators map cleanly onto a table.
| Option | Source of truth | Branching model | Deployment mechanism | Per-stage config | When appropriate |
|---|---|---|---|---|---|
| 1 — Git integration | Git | Gitflow (a primary branch per stage) | Fabric Git APIs (update-from-git) |
Separate per-stage branches | Git as single source of truth, Gitflow teams, no build-time transform |
| 2 — Fabric Items APIs | Git (single Main) | Trunk-based | Fabric Items APIs (fabric-cicd or Bulk Import) | Build-environment scripts | Trunk-based teams that must transform item definitions before deploy |
| 3 — Deployment pipelines | Fabric workspace (Git only through dev) | Trunk-based | Deployment pipelines APIs | Deployment rules + autobinding | Fabric-native, low-code promotion between workspaces |
| 4 — CI/CD for ISVs | Git (single Main) | Trunk-based | Fabric Items APIs (per customer workspace) | Per-customer release parameters | ISVs managing hundreds/thousands of per-customer workspaces |
Option 1 uploads directly from the repo into the workspace; there is no build environment altering files before deployment, which is why it pairs with Gitflow and a branch per stage.
Option 2 exists precisely because some teams need a build step. You need it “to alter workspace-specific attributes, such as connectionId and lakehouseId, before deployment” (manage-deployment). In this option the Test and Prod workspaces are not Git-connected — item definitions are pushed to them through the Items APIs from Main. This is the fabric-cicd path.
Option 3 is the one whose source of truth is easy to misstate. It is the Fabric workspace, not Git — Git connects only through the dev stage, and promotion from dev happens workspace-to-workspace with deployment rules and autobinding. Deployments are linear and require separate pipeline permissions.
Option 4 is built on top of Option 2 for ISVs: each customer gets its own release with its own parameters, and releases run in parallel because they are independent.
Microsoft’s own recommendation for “the most efficient CI/CD experience” is a hybrid — connect the developer workspace to Git and promote with deployment pipelines from there (CI/CD overview). Treat the four as a design vocabulary, not mutually exclusive products.
Where item CI/CD ends and platform governance begins
Every option above moves item definitions. Git integration operates at the workspace level and “re-creates item definitions only and does not restore item data” (Git integration process). That boundary — definitions in, everything else out — is the whole story. Here is the concrete state Git integration does not manage by default:
- Workspace creation and naming. The Git connection is to an existing workspace. Nothing in the repo brings a workspace into existence, names it to a convention, or decommissions it.
- Capacity and domain assignment. Which capacity backs a workspace, and which domain it belongs to, are tenant-administrative acts. Assigning a domain “requires tenant-level Fabric Administrator privileges” (Fabric CLI
assign) — it is not a property Git syncs. - Tenant settings. Publish-to-web toggles, SP-access switches, delegation flags — none of these live in a workspace repo. They are read and written through the admin tenant-settings APIs.
- Connections and gateways. References to Connections don’t auto-bind on deployment; Microsoft’s guidance is to “use Variable Libraries with environment-specific value sets to manage connection references across environments” (cross-workspace dependency binding). The connection object itself is out-of-band state.
- Roles and access. Workspace role assignments — who is Admin, Member, Contributor, Viewer — are not carried in item definitions.
There is a subtler failure mode even within item deployment. Some items store dependencies as object IDs (workspace-specific GUIDs) rather than logical IDs (portable identifiers in the .platform file). Items using logical IDs bind to the target workspace; items using object IDs “remain pointed at the source workspace, which breaks the deployment” (cross-workspace dependency binding). Even Option 1 warns that some dependencies “require these post-deployment updates” through additional API calls (manage-deployment). The item pipeline is not self-sufficient; it assumes a governed platform already exists around it.
The declarative model: manifest → PR → service principal → provisioning
The missing layer has a reference implementation: Microsoft’s frontier-fabric-governance-rvas blueprint. Its governing principle is unambiguous:
Treat the Fabric tenant as declarative infrastructure. Nothing exists unless a YAML manifest for it lives in
main, was reviewed via Pull Request, and was provisioned by a service principal through GitHub Actions.
The change flow is literally PR → validate → OIDC provision → drift (README). A workspace manifest is a YAML file with a strict, validated shape — the sample manifest carries name, capacity, region, domain, subDomain, sensitivityLabel, costCenter, and an owners array with principalType/role/groupName. A JSON Schema enforces a naming pattern (<country>-<area>-<subject>-<dataProductType>-<env>-<suffix>) and rules like “at least two owners, at least one Group, at least one Admin.” Governance policy — approved capacities, domains, sensitivity labels, cost centers, quotas — lives in a separate rules/policy.yaml, designed so “you can evolve governance without changing scripts.” Provisioning is idempotent: provision.py looks a workspace up by display name, creates it if absent, reconciles capacity and description, and adds missing role assignments additively — it deliberately does not remove unknown assignments, to avoid locking the automation out of its own workspaces.
The blueprint spans exactly the state the item pipeline ignores: workspace lifecycle, domains/capacities/sensitivity/endorsement, group-based access with expiring assignments, medallion bootstrap, and cross-environment promotion via deployment pipelines. (Note it is a workshop/blueprint repo; its public workflows are dormant by design until you fork and register a runner.)
What belongs in Git: workspace and platform manifests, item definitions, parameter files, policy/rules files, JSON schemas, and pipeline/workflow definitions. What does not: secrets, client secrets, tokens, per-stage secret values, and ephemeral runtime state. The provisioner in the blueprint holds no client secret at all — credentials are federated, not stored. That is the model to copy.
Service principal design
Two distinct axes govern SP design: which admin API surface the identity can touch, and where its credential lives.
The admin-API surface
Fabric splits read from write with two separate tenant settings, each independently scoped to its own Entra security group:
- “Service principals can access read-only admin APIs” governs Power BI read-only admin APIs. An SP used here “must not have any admin-consent required permissions for Power BI set on it” (enable SP admin APIs). Membership in the allowed group grants read access to all the information available through admin APIs, current and future.
- “Service principals can access admin APIs used for updates” governs Fabric update APIs, “such as the Workspaces - Restore Workspace API” (enable SP admin APIs).
Because each toggle has its own “Specific security groups” scope, least privilege is implemented by separating identities: a read-only identity in the read group for inventory, drift detection, and reporting; a write identity in the update group for provisioning. Enabling the settings themselves requires Fabric admin rights, a control point separate from group membership. State one nuance accurately: the API that modifies tenant settings — POST /v1/admin/tenantsettings/{tenantSettingName}/update — is explicitly Preview, “not recommended for production use,” capped at 25 requests/minute, requiring Tenant.ReadWrite.All. Listing tenant settings carries no Preview banner (effectively GA); programmatic writes are explicitly Preview.
The credential
The blueprint’s identity model is the pattern to emulate: a single app registration (gh-fabric-workspace-provisioner) is “the only identity allowed to write to Fabric in production,” and “the SPN never holds a client secret; GitHub trades its short-lived OIDC token for a Fabric access token at runtime.” It carries three federated credentials scoped to distinct GitHub OIDC subject claims — pull_request (read-only checks), ref:refs/heads/main (drift), and environment:production (write, only after environment approval), all with audience api://AzureADTokenExchange. Its granted roles are Fabric Administrator, Capacity Admin per in-scope capacity, and MIP label-publishing membership — and humans do not get the Fabric Administrator role, ownership of the SPN, or the ability to push to main. Where a stored secret is unavoidable it belongs in Key Vault, never the repo; OIDC/federated credentials eliminate the secret entirely and are the preferred design.
Environment configuration without hardcoding
Per-stage values must be injected, never committed as literals into item definitions. Fabric gives you three mechanisms, matched to the three Git-based options.
Variable Library
The Fabric-native, GA approach. It is “a bucket of variables that other items in the workspace can consume,” where “each variable can have multiple value-sets… One value-set is designated as ‘active’ per workspace” and “you can have a different active value set for each stage of a deployment pipeline” (Variable Library overview). Consumers include pipelines, notebooks, Dataflow Gen2, Copy jobs, and lakehouse shortcuts. This is the recommended way to carry connection references across environments.
Deployment rules
These apply in Option 3. When promoting between stages you “configure deployment rules to change the content while keeping some settings intact” — e.g. pointing a production semantic model at a production database. Rules come in three types (data source, parameter, default lakehouse), “can’t be created in the development stage,” and “only take effect the next time you deploy” (create deployment rules).
fabric-cicd parameter.yml
Build-time transforms in Option 2. fabric-cicd performs a full deployment on every run — it “does not inspect commit history or compute diffs,” so “the target workspace always reflects the repository and converges to the desired state… preventing environment drift” (deployment overview). The environment argument selects which environment key is applied. The library exposes a small, keyword-only API — note token_credential is now required:
from azure.identity import AzureCliCredential
from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items
target_workspace = FabricWorkspace(
workspace_id="your-workspace-guid",
repository_directory="/path/to/repo",
item_type_in_scope=["Notebook", "DataPipeline", "Environment", "VariableLibrary"],
environment="PPE",
token_credential=AzureCliCredential(),
)
publish_all_items(target_workspace)
unpublish_all_orphan_items(target_workspace)
item_type_in_scope accepts the 29 supported types in exact casing: Notebook, DataPipeline, Lakehouse, Warehouse, SemanticModel, Report, Environment, Eventhouse, Eventstream, KQLDatabase, KQLQueryset, KQLDashboard, Dataflow, CopyJob, SparkJobDefinition, MirroredDatabase, GraphQLApi, SQLDatabase, UserDataFunction, VariableLibrary, MLExperiment, DataAgent, ApacheAirflowJob, DataBuildToolJob, MountedDataFactory, Map, PaginatedReport, Reflex, and Ontology. Omitting the argument defaults to all types.
A parameter.yml at the repo root drives the transform. Current fields are find_value, replace_value (an environment-keyed dict), and optional is_regex / ignore_case plus item_type / item_name / file_path filters — there is no input_type field:
find_replace:
- find_value: "db52be81-c2b2-4261-84fa-840c67f4bbd0"
replace_value:
PPE: "$items.Lakehouse.Example_LH.$id"
PROD: "$items.Lakehouse.Example_LH.$id"
is_regex: "true"
item_type: "Notebook"
file_path: "**/notebook-content.py"
key_value_replace:
- find_key: $.properties.activities[?(@.name=="Run Notebook")].typeProperties.notebookId
replace_value:
PPE: "$items.Notebook.Hello World.$id"
PROD: "$items.Notebook.Hello World.$id"
item_type: "DataPipeline"
find_replace does string substitution; key_value_replace does JSONPath key replacement. Dynamic tokens like $workspace.$id and $items.<Type>.<Name>.$id resolve against the target environment.
A concrete GitHub Actions reference flow
The flow that ties this together: OIDC login (no stored secret), install tooling, a validation gate that fails the PR before anything touches the tenant, and a deploy job gated behind an environment approval on merge to main.
name: fabric-deploy
on:
pull_request:
paths: ["workspaces/**", "src/items/**", "parameter.yml"]
push:
branches: [main]
paths: ["workspaces/**", "src/items/**", "parameter.yml"]
permissions:
id-token: write # required for OIDC
contents: read
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
allow-no-subscriptions: true
- uses: actions/setup-python@v5
with: { python-version: "3.11" }
- run: pip install fabric-cicd ms-fabric-cli
- name: Validate manifests and item scope (dry run)
run: python scripts/validate.py --changed-only
deploy:
if: github.ref == 'refs/heads/main'
needs: validate
runs-on: ubuntu-latest
environment: production # manual approval gate
steps:
- uses: actions/checkout@v4
- uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
allow-no-subscriptions: true
- uses: actions/setup-python@v5
with: { python-version: "3.11" }
- run: pip install fabric-cicd
- name: Publish item definitions
env:
TARGET_WORKSPACE_ID: ${{ vars.PROD_WORKSPACE_ID }}
run: python scripts/deploy.py --environment PROD
The Fabric CLI (fab, installed via pip install ms-fabric-cli) complements this for platform operations the item libraries don’t cover — it authenticates with a federated token (fab auth login --federated-token <token> --tenant <id>) and reaches admin surfaces directly (fab api -X get workspaces), including workspace creation (fab mkdir) and capacity/domain assignment (fab assign), the latter requiring Fabric Administrator rights. The blueprint’s own workflows follow this shape exactly: validate.yml on pull_request posts a report as a PR gate, provision.yml runs on push to main behind environment: production, and drift.yml runs nightly on cron, opening a GitHub issue when live state diverges from the manifests.
Anti-patterns
- Portal-only production changes. Any change made directly in a production workspace is invisible to Git and will be silently reverted by the next full fabric-cicd run, which “converges to the desired state regardless of what happened in previous runs” (deployment overview). The portal is for the dev workspace, not prod.
- Manual workspace creation. Clicking “New workspace” produces an unnamed, unpoliced, untracked object. If it isn’t a manifest in
main, it doesn’t exist. - Untracked tenant settings. Toggling SP-access or publish-to-web in the admin portal with no record is unauditable configuration drift at the most privileged layer of the platform.
- Secrets in the repo. Client secrets, tokens, and per-stage secret values never belong in Git. Use federated OIDC credentials, or Key Vault where a secret is unavoidable.
- No per-stage parameterization. Hardcoding a dev
lakehouseIdorconnectionIdinto an item definition guarantees a broken prod deploy. Use Variable Libraries, deployment rules, orparameter.yml. - Drift with no reconciliation. Without a scheduled drift job comparing manifests to live tenant state, governance is aspirational. Detection must open a tracked issue, as the blueprint’s nightly
drift.ymldoes.
Definition of done for tenant-as-code
You have reached tenant-as-code when: every workspace, capacity assignment, domain, role grant, and governed tenant setting originates from a version-controlled manifest merged through PR review; no human holds standing write access to production Fabric — a federated service principal provisions, and its credential is short-lived and secret-free; per-stage values are injected through Variable Libraries, deployment rules, or parameter.yml, never hardcoded; a scheduled job reconciles declared state against live state and raises a tracked issue on drift; and the only thing a portal is used for in production is reading. Item CI/CD gets you a repeatable deployment. Tenant-as-code gets you a platform whose entire configuration is reviewable, auditable, and replayable — which is the part Git integration was never going to give you.
References
Verified against Microsoft Learn (retrieved July 2026), the microsoft/fabric-cicd v1.2.0 docs and source, microsoft/frontier-fabric-governance-rvas, and the Fabric CLI docs. Preview features (the tenant-settings write API; the governance blueprint’s dormant workflows) should be re-checked against the live product before you rely on them.
- CI/CD workflow options in Fabric (Microsoft Learn)
- Introduction to CI/CD in Microsoft Fabric (Microsoft Learn)
- Overview of Fabric Git integration · Git integration process
- Cross-workspace dependency binding (Microsoft Learn)
- Variable Library overview · Create deployment rules
- Enable service principals to use admin APIs · List Tenant Settings · Update Tenant Setting (Preview)
- microsoft/fabric-cicd · docs · supported item types
- microsoft/frontier-fabric-governance-rvas
- Microsoft Fabric CLI (
fab)