# Microsoft's New Fabric Azure DevOps Extension: Promising Wrapper, Limited Value Today

Microsoft recently released a new Azure DevOps extension for Microsoft Fabric, bringing Fabric CLI usage directly into ADO Pipelines. The promise is simple: install the extension, drop a `FabricCLI@0` task into your YAML, and the Fabric CLI is automatically provisioned in the pipeline runtime. No manual `pip install`, no version pinning gymnastics. Just write your `fab` commands and go.

That's the promise. The reality, at the time of writing, takes a bit more work to get to — and once you're there, it's worth asking what the extension actually buys you.

In this post I'll walk through what the extension provides, the bumps I hit getting it to actually work, the to-go pipeline that does work, and the question I think is worth asking honestly: **does the extension currently give you anything you can't do in a few lines with** `AzureCLI@2` **and a** `pip install`**?**

👉 You can find the extension's source code on GitHub: [microsoft/ms-fabric-azure-devops-extensions](https://github.com/microsoft/ms-fabric-azure-devops-extensions).

👉 Install the extension from the [Azure DevOps Marketplace](https://marketplace.visualstudio.com/items?itemName=ms-fabric-api.fabric-automation-tools-ext).

👉 The launch announcement is here: [Fabric CLI in Azure DevOps: automation without friction (Preview)](https://blog.fabric.microsoft.com/en-US/blog/fabric-cli-in-azure-devops-automation-without-friction-preview/).

## What the extension provides on paper

The extension adds a single task `FabricCLI@0` to your ADO pipeline. The task auto-provisions the `fab` CLI at the version you pin, supports inline scripts or external script files, and works with PowerShell, PowerShell Core, Bash, and Batch. The README highlights:

*   **Zero-setup Fabric CLI**: The task installs `fab` for you
    
*   **Multi-platform scripting**: `ps`, `pscore`, `bash`, `batch`
    
*   **Inline or file-based scripts**
    
*   **Workspace management, item deployment, Git integration, deployment pipelines:** anything `fab` can do
    
*   **Version-pinned CLI** for reproducible builds
    

The expected pattern from the README is essentially this:

```yaml
- task: FabricCLITask@0
  displayName: 'Create Fabric Workspace'
  env:
    FAB_SPN_CLIENT_ID: $(FAB_SPN_CLIENT_ID)
    FAB_TENANT_ID: $(FAB_TENANT_ID)
    FAB_SPN_FEDERATED_TOKEN: $(FEDERATED_TOKEN)
  inputs:
    scriptType: inlineScript
    FabricCLIVersion: V1.5.0  
    inlineScript: |
	  fab mkdir "MyWorkspace.Workspace" -P capacityname=$FAB_CAPACITY_NAME
```

Clean, declarative, and on the surface a reasonable wrapper for the CLI.

## Use cases the extension targets

The set of scenarios the extension is aimed at is genuinely valuable:

*   **Workspace provisioning**: Create and configure Fabric workspaces, assign them to capacities, set permissions
    
*   **Item deployment**: Publish notebooks, lakehouses, semantic models, pipelines and more directly from your repo
    
*   **Git integration:** Connect workspaces to ADO Repos, commit, sync from branches
    
*   **Deployment pipelines**: Promote items across stages (Dev → Test → Prod) from your CI/CD
    
*   **End-to-end Fabric CI/CD** without managing the CLI installation in every pipeline
    

If you've been doing Fabric automation with raw REST API calls, or with `pip install ms-fabric-cli` scattered across pipelines, the goal of this extension is to remove that ceremony.

## The reality: three gaps between docs and code

Working through the documented examples, I hit three issues that prevented the extension from working out of the box.

### Issue 1: The task name in the docs doesn't match what's installed

The launch blog post tells you to use `FabricCLI@1`. The README in the source repo tells you to use `FabricCLITask@0`. The actually installed task, once the extension is added to your ADO organization, is `FabricCLI@0`. Three sources of truth, three different names, and only one works.

⚠️ **If you get** `A task is missing. The pipeline references a task called 'FabricCLITask'` or similar, it's almost certainly because you copied the YAML from the README. Use `FabricCLI@0`.

That's a documentation issue, not a code issue, but it costs you the first 10 minutes of trying the extension on a pipeline that simply won't validate.

### Issue 2: The `pscore` script type generates a malformed `pwsh` command line

This one is more serious. Some of the samples use `scriptLanguage: pscore` where the task constructs a PowerShell Core command line that looks like this in the agent log:

```text
"C:\Program Files\PowerShell\7\pwsh.exe" -NoLogo -NoProfile -NonInteractive "-ExecutionPolicy Unrestricted" -Command ". 'D:\a\_temp\fabricclitask_xxx.ps1'"
```

Notice `"-ExecutionPolicy Unrestricted"`. The flag and its value are wrapped in a single set of quotes, so `pwsh` receives them as one argument with an embedded space. `pwsh` rejects this with:

```text
The argument '-ExecutionPolicy Unrestricted' is not recognized as the name of a script file.
##[error]Script execution failed with exit code 64
```

Your `inlineScript` never runs. The task fails before reaching any of your code. I reproduced this on both `ubuntu-latest` and `windows-latest`, with task version `0.1.0`.

There's no YAML workaround for this. The broken argv is constructed inside the task's wrapper code. The fix is to **avoid the** `pscore` **code path entirely** by using `scriptLanguage: bash` (on Linux agents).

### Issue 3: The keyring problem on hosted Linux agents

Switching to `bash` gets your script running, but introduces a different error:

```text
x [EncryptionFailed] An error occurred with the encrypted cache.
Enable plaintext auth token fallback with 'config set encryption_fallback_enabled true'
```

The Fabric CLI uses the OS keyring (libsecret on Linux) to encrypt cached auth tokens. Microsoft-hosted Linux agents don't have a running keyring service, so the encrypted-cache call fails. The error message helpfully tells you the fix: enable the plaintext fallback.

The fix is to run the corresponding `fab config set` commands as the very first lines of your `inlineScript`, before any other `fab` command:

```yaml
inlineScript: |
  fab config set encryption_fallback_enabled true
  fab config set context_persistence_enabled false
  fab ls
```

Both settings matter on ephemeral CI agents. `encryption_fallback_enabled` lets `fab` store tokens unencrypted when no keyring is available. `context_persistence_enabled=false` skips trying to persist navigation context, which is meaningless across one-shot pipeline runs.

⚠️ This config is safe on hosted ephemeral agents. They're destroyed after each run and the federated token is short-lived anyway. Don't enable plaintext fallback on a self-hosted persistent agent without considering the implications.

## A small detour: authentication via Workload Identity Federation

Before showing the working pipeline, a quick note on authentication. The whole point of the modern setup is to **avoid storing client secrets in your variable groups**. We use Workload Identity Federation (WIF) so that ADO can mint a short-lived OIDC token for each pipeline run, which Entra trusts because of a federated credential on the SPN's app registration.

Setting that up the first time has its own learning curve. And yes, you may run into `AADSTS700213: No matching federated identity record found` errors along the way if the issuer/subject in Entra doesn't match exactly what ADO is presenting. The error message itself tells you the exact subject ADO sent; copy-paste that into the federated credential in Entra (don't retype) and verify will pass.

🔗 For the full federated setup, the official docs are here: [Configure workload identity federation in Azure DevOps](https://learn.microsoft.com/en-us/azure/devops/pipelines/release/configure-workload-identity)

⚠️ One thing the docs don't make obvious: federation working is *not the same as* the SPN being allowed to do anything in Fabric. Once auth succeeds, you'll still need:

*   Tenant setting **"Service principals can use Fabric APIs"** enabled (Fabric admin portal → Tenant settings → Developer settings), scoped to a security group containing your SPN
    
*   Tenant setting **"Service principals can create workspaces, connections, and deployment pipelines"** enabled if you want to provision workspaces
    
*   Your SPN added as a **Capacity Admin or Contributor** on the target Fabric capacity
    

You will find additional information in the documentation of the [Developer Tenant settings](https://learn.microsoft.com/en-us/fabric/admin/service-admin-portal-developer).

If you skip these, the extension will authenticate beautifully and then return `[Unauthorized]` on every command. Authentication and authorization are separate problems; the extension only solves the first.

## The to-go pipeline that actually works

Here's the pipeline that finally got `fab ls` to return real data after a fair amount of trial and error. Two tasks: one to fetch the federated token via WIF, one to run `fab` with the right config.

```yaml
trigger: none

pool:
  vmImage: 'ubuntu-latest'

steps:
  - task: AzureCLI@2
    displayName: 'Get federated token'
    inputs:
      azureSubscription: 'FabricServiceConnection'
      scriptType: bash
      scriptLocation: inlineScript
      addSpnToEnvironment: true
      inlineScript: |
        echo "##vso[task.setvariable variable=FAB_SPN_FEDERATED_TOKEN;issecret=true]$idToken"
        echo "##vso[task.setvariable variable=FAB_SPN_CLIENT_ID]$servicePrincipalId"
        echo "##vso[task.setvariable variable=FAB_TENANT_ID]$tenantId"

  - task: FabricCLI@0
    displayName: 'List Fabric Workspace'
    env:
      FAB_SPN_CLIENT_ID: $(FAB_SPN_CLIENT_ID)
      FAB_TENANT_ID: $(FAB_TENANT_ID)
      FAB_SPN_FEDERATED_TOKEN: $(FAB_SPN_FEDERATED_TOKEN)
    inputs:
      scriptType: inlineScript
      scriptLanguage: bash
      FabricCLIVersion: 'v1.5.0'
      inlineScript: |
        fab config set encryption_fallback_enabled true
        fab config set context_persistence_enabled false
        fab ls
```

A few things worth highlighting:

*   `addSpnToEnvironment: true` on `AzureCLI@2` is what exposes `$idToken`, `$servicePrincipalId`, and `$tenantId` from your WIF service connection
    
*   `scriptLanguage: bash` sidesteps the `pscore` argument-quoting bug
    
*   `fab config set` **lines are the very first thing in** `inlineScript`. They have to run before any `fab` command that touches auth or cache state
    
*   `FAB_SPN_*` **env vars on the task** pass the federated token and SPN identity into the `fab` process
    

If you're targeting multiple Fabric tasks in one pipeline, my recommendation is to wrap each Fabric-using task in its own `AzureCLI@2` step (or fetch a fresh token per task). The OIDC `idToken` from ADO is short-lived (~10 minutes), so a long-running pipeline that fetches once and reuses can hit token expiry mid-run.

## The question worth asking: is the extension actually adding value?

This is where I want to be honest. The extension's headline benefit is "zero-setup Fabric CLI." Look at what that's worth:

```yaml
pip install --quiet --user ms-fabric-cli==1.5.0
export PATH="$HOME/.local/bin:$PATH"
```

Two lines. That's the "setup" the extension is saving you.

Here's the same end-to-end workflow done with vanilla `AzureCLI@2`... so no extension needed:

```yaml
trigger: none

parameters:
  - name: workspaceName
    type: string
    default: 'MyExtensionCreatedWorkspace'

pool:
  vmImage: 'ubuntu-latest'

steps:
  - task: AzureCLI@2
    displayName: 'Run Fabric CLI'
    inputs:
      azureSubscription: 'FabricServiceConnection'
      scriptType: bash
      scriptLocation: inlineScript
      addSpnToEnvironment: true
      inlineScript: |
        set -e
        pip install --quiet --user ms-fabric-cli==1.5.0
        export PATH="$HOME/.local/bin:$PATH"

        export FAB_SPN_CLIENT_ID="$servicePrincipalId"
        export FAB_TENANT_ID="$tenantId"
        export FAB_SPN_FEDERATED_TOKEN="$idToken"
        
        fab config set encryption_fallback_enabled true
        fab config set context_persistence_enabled false
        fab auth login
        fab ls
```

**Single task. No extension required. No org-level install. No marketplace dependency. No bugs to work around.**

Compared to the extension version:

| Aspect | Extension (`FabricCLI@0`) | Plain `AzureCLI@2` + `pip install` |
| --- | --- | --- |
| Tasks needed | 2 (token fetch + Fabric CLI) | 1 |
| Lines of YAML | ~30 | ~30 |
| Org admin install | Required | Not required |
| `fab` auto-installed | ✅ | One-line `pip install` |
| Authentication style | Identical (WIF) | Identical (WIF) |

As it stands, the extension is essentially a thin wrapper on the CLI head. Its value-add today comes down to running `pip install ms-fabric-cli` for you, and that's about it. Everything else, authentication, configuration, and the actual `fab` commands, is something you write yourself.

## Verdict

The extension is a reasonable starting point and clearly the right direction for first-class Fabric CI/CD in Azure DevOps. But in its current preview state, the official approach **costs you more setup time than it saves**, and the documentation is materially out of sync with the shipped code. As I see it today, there's no real value in using the extension over a plain `AzureCLI@2 + pip install` step. It's a thin wrapper around the CLI, and right now it's a wrapper that adds friction rather than removing it.

If you want to try it today, here's my honest recommendation:

*   **Use the to-go pipeline above.** It works. Use `bash`, not `pscore`.
    
*   **File issues on the repo.** 👉 [microsoft/ms-fabric-azure-devops-extensions — Issues](https://github.com/microsoft/ms-fabric-azure-devops-extensions/issues). The maintainers genuinely need to hear about the `pscore` argument bug, the keyring/wrapper ordering issue, and the doc/code mismatches. Constructive feedback now is how preview features improve.
    
*   **Don't feel bad about using** `AzureCLI@2 + pip install` **instead.** It's not a hack. It's the same authentication, the same CLI, the same `fab` commands. Until the extension provides meaningful value beyond running `pip install` for you, the plain approach is simpler, has fewer moving parts, and doesn't depend on a marketplace install your org admin has to manage.
    
*   **Or go further with a custom build accelerator.** If you're doing serious Fabric automation, a thin task wrapper is rarely where the real productivity gains come from. Purpose-built helpers, recipe files, and reusable scripts that encode your team's conventions are what actually move the needle. 👉 That's the approach I take in my own [FabricOps repo](https://github.com/gronnerup/FabricOps), where the goal is to give you opinionated building blocks for Fabric CI/CD rather than another way to invoke `fab`.
    

I'll update this post (or write a follow-up) once the extension hits a state where it earns its place over the plain approach. The potential is there. The execution just needs another iteration or two.

Until then... automate everything, automate it smartly, and don't be afraid to skip a tool that isn't ready yet 😉
