๐ก๏ธ Azure DevOps CI Quality Gate Execution Flow
Here is the exact implementation flow executing on every Pull Request targeting main:
Git PR
โ
โ
Azure DevOps CI
โ
โ
Checkout PR branch
โ
โ
Download Tabular Editor 2.28
โ
โ
Load TMDL model
โ
โ
-A BPARules.json
โ
โ
Run BPA
โ
โโโโโโโโโโโดโโโโโโโโโโโ
โ โ
No violations Severity 3
โ violation
โ โ
ExitCode = 0 ExitCode != 0
โ โ
โ Azure DevOps
โ logging via -V
โ โ
โ โ
โ $process.ExitCode
โ โ
โ `-ne 0` = TRUE
โ โ
โ โ
โ exit 1
โ โ
โ โ
โ
CI passes โ CI fails
Why Severity 3 was the Correct Choice
Your business rule is effectively: "These model-quality violations must prevent a PR from being accepted."
The official Tabular Editor 2 documentation confirms that Severity ≥ 3 is reported as an error when -V is used.
The 5 Things to Remember in the CI Pipeline
| Item | Role in your Pipeline |
|---|---|
| BPARules.json | Defines what is considered an architectural violation. |
| Severity: 3 | Makes those violations errors that fail the linter. |
| -A | Runs Tabular Editor BPA using your custom JSON rulebook. |
| -V | Emits Azure DevOps/VSTS logging commands (##vso[task.logissue type=error]). |
| $process.ExitCode | PowerShell accurately reads Tabular Editor's process result without stream corruption. |
| exit 1 | Explicitly fails the Azure DevOps build task, locking the PR merge gate. |
Why Branch Policy is Architecturally Superior (Prevention vs Detection)
Branch policies ensure that no broken code is merged into main. A bad TMDL definition can reach the Fabric DEV workspace and break the shared development environment for the entire engineering team.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ ๐ด WITHOUT BRANCH POLICY โ โ ๐ข WITH BRANCH POLICY โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ Developer Developer โ โ โผ โผ Feature Branch Feature Branch โ โ โผ โผ Pull Request โ main Pull Request โ main โ โ โผ โผ โ No validation gate ๐ Branch Policy โ Build Validation REQUIRED โผ โ MERGE โผ โ Azure DevOps CI โผ โ main โ contains โผ bad TMDL Tabular Editor + BPA โ โ โผ โผ CI runs Severity 3 violation โ โ โผ โผ Tabular Editor + BPA CI โ FAILS โ โ โผ โผ Severity 3 violation PR MERGE BLOCKED ๐ โ โ โผ โผ CI โ FAILS main CLEAN โ โ โ โผ โผ But bad code is already in Fabric protected from main this bad change โ โผ Git โ Fabric Synchronization / Updates โ โผ DEV Workspace potentially receives bad definition โ โผ โ Shared Fabric DEV can be broken
| Approach | When is problem detected? | Can bad code enter main? | Impact |
|---|---|---|---|
| CI after merge | After merge | โ Yes | Shared environment and workspace can be affected and broken. |
| PR Build Validation (Branch Policy) | Before merge | โ No | Problem remains completely isolated to the feature PR branch! |
๐ Enterprise SPN, Security Group & Two-Stage Automated CD Architecture
Full live production architecture: Microsoft Entra ID (AAD) + Fabric Tenant Settings + Azure DevOps Multi-Stage CI/CD with Production Approval Gate.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. IDENTITY & ACCOUNTS โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ AZURE DEVOPS OWNER & APPROVER โ FABRIC TENANT GLOBAL & FABRIC ADMIN โ
โ Account: punitgir74@gmail.com โ Account: punitgiri@punitgiri921... โ
โ Org: dev.azure.com/punitgiri74 โ ...gmail.onmicrosoft.com โ
โ Project: Brightline-Analytics โ Roles: Global Admin + Fabric Adminโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ
โ โ
โ Configures CI/CD & Approval Gate โ Elevates Roles & Tenant
โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 2. MICROSOFT ENTRA ID (AAD) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ TENANT ID: 4f040b4b-xxxx-xxxx-xxxx-xxxxxxxxxxxx โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ APP REGISTRATION (SERVICE PRINCIPAL) โ โ SECURITY GROUP (UI BRIDGE) โ โ
โ โ Name: Brightline-Fabric-Deployer โ โ Name: Fabric-Pipeline-Admins โ โ
โ โ Client ID: 92c2b003-xxxx-xxxx-xxxx-xxxxxxxxxxxx โ ---> โ Object ID: 134202bc-xxxx-xxxx-xxxx-... โ โ
โ โ Secret: โขโขโขโขโขโขโขโขโขโขโขโขโขโขโขโขโขโขโขโขโขโขโขโขโขโขโขโขโขโขโขโข โ โ Members: Brightline-Fabric-Deployer โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ Owners: punitgiri921... & 74gmail โ โ
โ โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโ
โ
Grants Admin permissions โ
across Fabric resources โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 3. MICROSOFT FABRIC PLATFORM โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ A. ADMIN PORTAL (TENANT SETTINGS -> DEVELOPER SETTINGS) โ
โ โข Service principals can create workspaces, connections, and deployment pipelines: ENABLED โ
โ โข Service principals can call Fabric public APIs: ENABLED โ
โ โข Target Audience: Fabric-Pipeline-Admins โ
โ โ
โ B. DEPLOYMENT PIPELINE ACCESS (Brightline-Sales-Pipeline: d116d0bb-xxxx-xxxx-xxxx-xxxxxxxxxxxx) โ
โ โข Pipeline Access: Fabric-Pipeline-Admins -> ADMIN โ
โ โ
โ C. WORKSPACE ACCESS โ
โ โข BL-Sales-DEV -> Brightline-Fabric-Deployer -> ADMIN โ
โ โข BL-Sales-Test -> Brightline-Fabric-Deployer -> ADMIN โ
โ โข BL-Sales-Prod -> Brightline-Fabric-Deployer -> ADMIN โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โฒ
โ REST API Calls via OAuth 2.0 Bearer Token
โ (POST /v1.0/myorg/pipelines/{pipelineId}/deployAll)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 4. AZURE DEVOPS MULTI-STAGE CI/CD FLOW โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ VARIABLE GROUP: Brightline-pipeline-variables โ
โ [ AZURE_TENANT_ID | AZURE_CLIENT_ID | AZURE_CLIENT_SECRET (Locked ๐) | FABRIC_PIPELINE_ID ] โ
โ โ
โ [ Developer Merges Pull Request to 'main' ] โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ STAGE 1: Brightline Build, Lint & Deploy to Test โ โ
โ โ 1. Tabular Editor 2 CLI executes BPARules.json model quality checks โ โ
โ โ 2. Authenticates via Entra ID SPN token โ โ
โ โ 3. SPN calls POST .../pipelines/{id}/deployAll (sourceStageOrder: 0) โ โ
โ โ 4. RESULT: BL-Sales-DEV โโโโโโโโโโ Promoted Automatically โโโโโโโโโ> BL-Sales-Test โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ STAGE 2: Brightline Production Release Gate (GOVERNANCE APPROVAL) โ โ
โ โ โข Environment: Brightline-Production-Environment โ โ
โ โ โข Check: Halts execution & sends email notification to: punitgir74@gmail.com โ โ
โ โ โข Action: Human reviews Test stage & clicks [ Approve ] in Azure DevOps โ โ
โ โ โ โ
โ โ [ Upon Human Approval ] โ โ
โ โ 5. SPN calls POST .../pipelines/{id}/deployAll (sourceStageOrder: 1) โ โ
โ โ 6. RESULT: BL-Sales-Test โโโโโโโโโ Promoted to Production โโโโโโโโ> BL-Sales-Prod โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ The 6-Step Implementation Ledger & Security Configuration
Elevated punitgiri@punitgiri921gmail.onmicrosoft.com to:
- Fabric Administrator: Full control over Fabric tenant settings and capacities.
- Global Administrator: Full administrative control over Microsoft Entra ID directory.
Navigated to Home > Default Directory > App registrations:
Created Cloud Security Group in Microsoft Entra ID:
In Admin portal > Tenant settings > Developer settings:
- Service principals can create workspaces, connections, & deployment pipelines: Enabled -> Target:
Fabric-Pipeline-Admins - Service principals can call Fabric public APIs: Enabled -> Target:
Fabric-Pipeline-Admins
Granted access across all deployment boundary layers:
Configured Library Variable Group & Production Environment:
- Variable Group:
Brightline-pipeline-variableswith masked secret ๐ - Environment:
Brightline-Production-Environmentwith Approverpunitgir74@gmail.com - API Endpoint: Uses
POST .../pipelines/{id}/deployAll(replaces single-item /deploy)
Occurs when the SPN acquires an Entra ID token successfully, but Power BI rejects it because the SPN is missing from the Pipeline access list or the tenant-level switch is disabled.
Calling
/deploy expects explicit arrays of report/dataset GUIDs. Calling /deployAll promotes all supported stage items without requiring item IDs.
The Power BI Deployment Pipeline UI validates for email syntax, rejecting raw App IDs. Encapsulating the SPN inside a Security Group seamlessly bridges the UI requirement.
โ๏ธ Fabric Git Integration: Live Runtime vs Code Repository
A Fabric workspace is not just a folder containing .pbix files — it is a live semantic model & report runtime. Git is the source representation.
โ
โ "I have new changes"
↓
Fabric Source Control → Updates Tab
โ
โ Developer inspects diff & clicks "Update all"
↓
Fabric Workspace (Live Runtime becomes Version B)
Why Race Conditions Matter
Imagine two developers working concurrently:
โข Meanwhile, Developer B: Pushes TMDL commits to Git.
If synchronization happened automatically without approval, Developer B's Git push would silently overwrite Developer A's in-flight work!
Therefore, Git is the source-controlled definition, while the workspace is the live runtime. The Updates tab gives developers deliberate control over when Git changes are compiled into the live cloud workspace.
main → Fabric Git Updates deploys it to workspace → Azure DevOps can promote it through Fabric pipelines using an SPN.
Your notes are focusing on the architecture, but if you don't know BPA and Tabular Editor, let's build from zero.
1. First: What is Tabular Editor?
Think of Power BI Desktop as the GUI for building a semantic model.
You normally create:
- Tables
- Relationships
- Measures
- Calculation groups
- Columns
- Perspectives
- Display folders
- Formatting
- Model properties
But Power BI Desktop isn't always the best tool for large-scale model development and automation.
That's where Tabular Editor comes in.
Simple analogy
You already know VBA/Excel. Think:
| Excel world | Power BI world |
|---|---|
| Excel GUI | Power BI Desktop |
| VBA | Tabular Editor scripting |
| Workbook structure | Semantic model |
| VBA validation/checks | BPA |
| Git | Source control |
| Automated VBA checks | CI validation |
Tabular Editor is a specialized tool for working with Tabular models, including Power BI semantic models.
2. What can Tabular Editor do?
Suppose you have 500 measures.
In Power BI Desktop, manually changing properties on 500 measures would be painful.
Tabular Editor can automate things like:
→ Display Folder = Sales
→ Format = $#,##0
→ Description = "Total sales amount"
Gross Margin
→ Display Folder = Profitability
→ Format = 0.0%
You can also use scripting to perform bulk operations.
For example:
{
measure.FormatString = "#,##0";
}
So instead of manually modifying hundreds of objects, you can automate the model metadata.
3. Now what is BPA?
This is probably the most important missing concept in your notes.
BPA = Best Practice Analyzer.
It is essentially a rule engine that checks your semantic model against predefined best practices.
Imagine your Power BI model contains:
- FactSales
- DimCustomer
- DimProduct
- DimDate
Someone creates a measure:
BPA can check things such as:
- Does the measure have a description?
- Are naming conventions followed?
- Are unnecessary columns exposed?
- Are relationships configured properly?
- Are certain modeling patterns being violated?
- Are there potentially problematic DAX patterns?
BPA = rules that inspect the model.
4. BPA is basically a model quality gate
Imagine your company defines this rule:
Every measure must have a description.
Developer creates:
but doesn't provide a description.
BPA detects: โ Measure "Total Sales" has no description
Another rule might say:
Measures should not be named Measure 1, Measure 2, etc.
BPA could detect: โ Invalid measure naming convention
So BPA becomes an automated quality-control layer.
5. What does Severity mean?
This connects directly to your note:
Think of BPA rules as having different levels. For example:
Severity 2 → Warning
Severity 3 → Error
The important thing is that Severity 3 can be configured to fail the automated validation process.
For example:
↓
Tabular Editor
↓
Run BPA
↓
BPA finds Severity 3 violation
↓
Exit code = 1
↓
CI pipeline sees failure
↓
โ Build fails
That's what your note means by:
"Tabular Editor to output an error stream and set a non-zero exit code (1)"
The important concept is exit code.
6. What is an exit code?
This is an important DevOps concept.
A command executed by a pipeline usually returns a status. Simplified:
1 = Failure
Suppose Azure DevOps executes:
If everything passes:
Exit code: 0
Azure DevOps says: Good — continue.
But if a Severity 3 BPA rule fails:
Exit code: 1
Azure DevOps says: Something failed → fail this pipeline step.
So BPA isn't merely showing a red warning on someone's screen. It can become an automated CI gate.
7. Now the "Shift-Left" concept
This is a very common DevOps architecture principle.
Imagine this workflow:
Developer
↓
Git
↓
main
↓
Fabric DEV
↓
Everyone discovers broken model
Developer
↓
Feature Branch
↓
BPA validation → โ Error → STOP
↓ (or)
BPA validation → โ Pass
↓
Pull Request → main
That's shift-left. You're catching the problem before it reaches main.
8. Why is this particularly important with Power BI + Git?
This is where your note is very good.
A Fabric workspace is not just a folder containing .pbix files.
You have a live semantic model/report runtime.
Your Git repository contains the source representation. Conceptually:
โ
โ source code / TMDL
↓
Fabric Workspace
โ
โ compiled/deployed model
↓
Live Power BI environment
Therefore, if bad model metadata gets merged into main, you can potentially push bad definitions toward the development workspace.
That's why:
9. What exactly is TMDL?
This is another important connection.
TMDL = Tabular Model Definition Language.
Instead of keeping your entire semantic model as one opaque file, the model can be represented as structured text.
For example, conceptually:
name: SalesModel
table Sales
column SalesAmount
measure Total Sales = SUM(...)
This is much more Git-friendly than treating the model as one large binary artifact.
So your architecture becomes:
10. Now the "Updates" tab
This is a completely different part of the architecture.
Your note says:
Fabric workspaces are live runtimes. Git is a code repository.
This distinction is very important.
Suppose Git contains Version A, and your Fabric workspace currently contains Version A. A developer changes Git to Version B. Does the workspace automatically become Version B?
Not necessarily in the way you should conceptualize it.
There is a synchronization process between Git source and the workspace state. The Updates experience lets you control when Git changes are applied/synchronized into the workspace.
Think:
โ
โ "I have new changes"
↓
Updates
โ
โ developer chooses to apply/sync
↓
Fabric Workspace
This matters because workspace development can also happen directly through Fabric/Power BI authoring experiences. So you don't want to blindly overwrite someone's live changes.
11. Why race conditions matter
Imagine:
Meanwhile
Developer B: Pushes Git changes → Changes semantic model
Now you have:
Developer A → Workspace
Developer B → Git
If synchronization is uncontrolled, you can potentially overwrite or conflict with work.
Therefore: Git is the source-controlled definition, while the workspace is the live working/runtime environment.
The Updates mechanism gives you control over when Git changes are applied.
12. Now SPN
This is the next major concept.
SPN = Service Principal Name, commonly used to refer to an Entra ID service principal in this automation context.
Don't think of it as a human user. Think:
versus
Application identity: Azure DevOps automation
The SPN is essentially an identity used by automation.
13. Why do we need an SPN?
Suppose Azure DevOps needs to tell Fabric:
"Deploy this pipeline from DEV to TEST."
Should Azure DevOps log in using your personal Microsoft account? No. That's fragile and bad practice.
Instead:
↓
Service Principal
↓
Entra ID
↓
Access token
↓
Fabric REST API
↓
Deployment
Now the deployment is performed using a controlled machine/application identity.
14. Client credentials flow
Your note says: client_id + client_secret
This is referring to the OAuth 2.0 client credentials flow. Very simplified:
- Azure DevOps has:
client_idandclient_secret - It sends these credentials to Entra ID.
- Entra ID verifies them.
- Then Entra ID returns: Access Token
- Azure DevOps then sends:
Authorization: Bearer <token>to the Fabric REST API.
โ
โ client_id + client_secret
↓
Entra ID
โ
โ Bearer token
↓
Azure DevOps
โ
โ REST API request
↓
Fabric
15. Why does the SPN need permissions?
Having an identity isn't enough. Fabric needs to know: "Is this identity allowed to perform this operation?"
Your note says: The SPN must be an Admin on the pipeline.
So conceptually:
โ
โโโ Authentication → Entra ID
โ
โโโ Authorization → Fabric
↓
Pipeline Admin
Authorization answers: What are you allowed to do?
This distinction is extremely important.
16. Putting ALL four concepts together
Now we can connect your entire table. Imagine a professional Power BI/Fabric development setup:
โ
↓
Git Feature Branch
โ
↓
TMDL model changes
โ
↓
Tabular Editor + BPA
โ
โโโโโโโโโดโโโโโโโโโ
โ โ
Pass Error
โ โ
↓ ↓
Continue โ Stop
โ
↓
Pull Request
โ
↓
main
โ
↓
Fabric Git Integration
โ
↓
Updates
โ
↓
DEV Workspace
โ
↓
Fabric Deployment
โ
↓
Azure DevOps + SPN + REST API
โ
↓
TEST / PROD
That is the bigger architecture your four notes are describing.
17. The key distinction you should remember
| Concept | What it does |
|---|---|
| Tabular Editor | Develop/modify/automate tabular semantic models |
| BPA | Checks semantic model against rules |
| Severity 3 | Treats a BPA violation as an error that can fail CI |
| Shift-left | Find problems before they reach main/shared environments |
| TMDL | Text-based representation of tabular model definitions |
| Git | Stores/version-controls the model source |
| Updates | Controls synchronization of Git changes into Fabric workspace |
| SPN | Machine/application identity for automation |
| Entra ID | Authenticates the SPN and issues token |
| Bearer token | Credential used to call Fabric REST API |
| Fabric pipeline | Promotes/deploys content between environments |
| Azure DevOps | Automates CI/CD process |
18. One thing I want you to be careful about
Don't mentally combine BPA and Tabular Editor as if they are the same thing. Think:
And don't confuse:
- Git → source/control
- with Fabric workspace → live environment
- and Deployment pipeline → promotion between environments
These are three different layers.
Your mental model
Git / TMDL
↓
QUALITY
Tabular Editor / BPA / CI
↓
RUNTIME
Fabric Workspace
↓
PROMOTION
Fabric Deployment Pipeline
↓
AUTOMATION
Azure DevOps + SPN + REST API
That mental model will make a lot of the advanced Fabric CI/CD topics much easier.
A good next step is to test whether you can distinguish Tabular Editor, BPA, Git, Fabric Updates, and Deployment Pipelines in a real CI/CD scenario.
๐ Pipeline Configuration Files & Code Manifest (Click to Expand / Collapse)
4 Files โข Live Production Code
โถ
๐ 1. Azure DevOps Unified Multi-Stage CI/CD Pipeline
azure-pipelines-ci.yml
Multi-Stage CI/CD (Live)
azure-pipelines-ci.yml
Full 2-stage production pipeline: Stage 1 runs headless Tabular Editor 2 BPA quality checks and auto-promotes DEV to TEST via deployAll. Stage 2 halts at Brightline-Production-Environment, sends an approval email to punitgir74@gmail.com, and promotes to Production upon manual review.
# ==============================================================================
# BRIGHTLINE ANALYTICS โ ENTERPRISE MULTI-STAGE CI/CD PIPELINE
# Stage 1: Brightline Quality Gate (BPA) & Automated Dev -> Test Deployment
# Stage 2: Brightline Production Promotion (Test -> Prod) with Approval Gate
# ==============================================================================
trigger:
branches:
include:
- main
pr:
branches:
include:
- main
pool:
vmImage: 'windows-latest'
variables:
- group: Brightline-pipeline-variables
- name: TE_VERSION
value: '2.28.0'
- name: MODEL_DIR
value: '$(Build.SourcesDirectory)/src/Brightline_Sales.SemanticModel/definition'
- name: BPA_RULES_PATH
value: '$(Build.SourcesDirectory)/bpa-rules/BPARules.json'
stages:
# ------------------------------------------------------------------------------
# STAGE 1: BRIGHTLINE CI QUALITY VALIDATION & AUTOMATED TEST DEPLOYMENT
# ------------------------------------------------------------------------------
- stage: Brightline_Build_And_Test
displayName: 'Brightline Build, Lint & Deploy to Test'
jobs:
- job: Validate_And_Deploy_Test
displayName: 'Validate BPA & Promote to Test Stage'
steps:
- checkout: self
displayName: 'Checkout Repository Code'
- task: PowerShell@2
displayName: 'Setup Tabular Editor CLI & Execute BPA Linter'
inputs:
targetType: 'inline'
script: |
Write-Host "============================================================"
Write-Host " ๐ BRIGHTLINE CI: TABULAR MODEL QUALITY VALIDATION"
Write-Host "============================================================"
$teDownloadUrl = "https://github.com/TabularEditor/TabularEditor/releases/download/$(TE_VERSION)/TabularEditor.Portable.zip"
$teZip = "$(Agent.TempDirectory)/TabularEditor.zip"
$teExtractDir = "$(Agent.TempDirectory)/TabularEditor"
Write-Host "1. Downloading Tabular Editor Portable CLI v$(TE_VERSION)..."
Invoke-WebRequest -Uri $teDownloadUrl -OutFile $teZip
Expand-Archive -Path $teZip -DestinationPath $teExtractDir -Force
$teExe = "$teExtractDir/TabularEditor.exe"
if (!(Test-Path $teExe)) {
Write-Error "TabularEditor.exe executable was not found at $teExe"
exit 1
}
Write-Host "Tabular Editor CLI ready at: $teExe"
Write-Host "`n2. Validating TMDL Model against Enterprise BPA Rules..."
Write-Host " Model Directory : $(MODEL_DIR)"
Write-Host " BPA Rules Path : $(BPA_RULES_PATH)"
$process = Start-Process -FilePath $teExe -ArgumentList @("$(MODEL_DIR)", "-A", "$(BPA_RULES_PATH)", "-V") -NoNewWindow -PassThru -Wait
if ($process.ExitCode -ne 0) {
Write-Host "##vso[task.logissue type=error]โ Model validation failed! Best Practice Analyzer violations detected."
Write-Error "CI Gate Failed. Please review the BPA violations above."
exit 1
} else {
Write-Host "============================================================"
Write-Host " โ
SUCCESS: All Best Practice Analyzer checks passed cleanly!"
Write-Host "============================================================"
}
- task: PowerShell@2
displayName: 'Automate Fabric Deployment Pipeline Promotion (Dev -> Test)'
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
inputs:
targetType: 'inline'
script: |
Write-Host "============================================================"
Write-Host " ๐ข BRIGHTLINE DEPLOYMENT: DEV -> TEST"
Write-Host "============================================================"
Write-Host "1. Authenticating with Microsoft Entra ID via SPN..."
$tokenUri = "https://login.microsoftonline.com/$(AZURE_TENANT_ID)/oauth2/v2.0/token"
$body = @{
client_id = "$(AZURE_CLIENT_ID)"
client_secret = "$(AZURE_CLIENT_SECRET)"
scope = "https://analysis.windows.net/powerbi/api/.default"
grant_type = "client_credentials"
}
$tokenResponse = Invoke-RestMethod -Uri $tokenUri -Method Post -Body $body
$headers = @{
Authorization = "Bearer $($tokenResponse.access_token)"
"Content-Type" = "application/json"
}
Write-Host "Authentication successful. Access token acquired."
Write-Host "`n2. Triggering Deployment Pipeline Promotion (DEV -> TEST)..."
$deployUrl = "https://api.powerbi.com/v1.0/myorg/pipelines/$(FABRIC_PIPELINE_ID)/deployAll"
# sourceStageOrder: 0 represents DEV -> TEST
$payload = @{
sourceStageOrder = 0
options = @{
allowCreateArtifact = $true
allowOverwriteArtifact = $true
}
note = "Automated promotion to Test via Azure DevOps Build $(Build.BuildNumber)"
} | ConvertTo-Json
try {
$deployJob = Invoke-RestMethod -Uri $deployUrl -Method Post -Headers $headers -Body $payload
Write-Host "Deployment operation initiated successfully. Operation ID: $($deployJob.id)"
} catch {
Write-Host "##[error]Deployment to Test failed!"
if ($_.Exception.Response) {
$stream = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($stream)
Write-Host "##[error]$($reader.ReadToEnd())"
} else {
Write-Host "##[error]$($_.Exception.Message)"
}
exit 1
}
Write-Host "`n3. Monitoring deployment status until completion..."
$statusUrl = "https://api.powerbi.com/v1.0/myorg/pipelines/$(FABRIC_PIPELINE_ID)/operations/$($deployJob.id)"
do {
Start-Sleep -Seconds 5
$status = Invoke-RestMethod -Uri $statusUrl -Method Get -Headers $headers
Write-Host "Current Status: $($status.status)"
} while ($status.status -eq "NotStarted" -or $status.status -eq "Executing")
if ($status.status -ne "Succeeded") {
Write-Error "โ Deployment to Test failed with status: $($status.status)"
exit 1
}
Write-Host "============================================================"
Write-Host " ๐ SUCCESS: Fabric Test Stage (BL-Sales-Test) Deployed!"
Write-Host "============================================================"
# ------------------------------------------------------------------------------
# STAGE 2: BRIGHTLINE PRODUCTION PROMOTION (Gated by Approval Environment)
# Pauses here, sends email to punitgiri74@gmail.com, and waits for your approval!
# ------------------------------------------------------------------------------
- stage: Brightline_Production_Promotion
displayName: 'Brightline Production Promotion (Test -> Prod)'
dependsOn: Brightline_Build_And_Test
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: Promote_To_Production
displayName: 'Brightline Production Release Gate'
environment: 'Brightline-Production-Environment'
strategy:
runOnce:
deploy:
steps:
- checkout: none
- task: PowerShell@2
displayName: 'Execute Fabric Deployment Pipeline Promotion (Test -> Prod)'
inputs:
targetType: 'inline'
script: |
Write-Host "============================================================"
Write-Host " ๐ BRIGHTLINE DEPLOYMENT: TEST -> PRODUCTION"
Write-Host " Approved by: $(Build.RequestedFor) (punitgiri74@gmail.com)"
Write-Host "============================================================"
Write-Host "1. Authenticating with Microsoft Entra ID via SPN..."
$tokenUri = "https://login.microsoftonline.com/$(AZURE_TENANT_ID)/oauth2/v2.0/token"
$body = @{
client_id = "$(AZURE_CLIENT_ID)"
client_secret = "$(AZURE_CLIENT_SECRET)"
scope = "https://analysis.windows.net/powerbi/api/.default"
grant_type = "client_credentials"
}
$tokenResponse = Invoke-RestMethod -Uri $tokenUri -Method Post -Body $body
$headers = @{
Authorization = "Bearer $($tokenResponse.access_token)"
"Content-Type" = "application/json"
}
Write-Host "Authentication successful."
Write-Host "`n2. Triggering Deployment Pipeline Promotion (TEST -> PROD)..."
$deployUrl = "https://api.powerbi.com/v1.0/myorg/pipelines/$(FABRIC_PIPELINE_ID)/deployAll"
# sourceStageOrder: 1 represents TEST -> PROD
$payload = @{
sourceStageOrder = 1
options = @{
allowCreateArtifact = $true
allowOverwriteArtifact = $true
}
note = "Production Release authorized via Azure DevOps Build $(Build.BuildNumber)"
} | ConvertTo-Json
try {
$deployJob = Invoke-RestMethod -Uri $deployUrl -Method Post -Headers $headers -Body $payload
Write-Host "Production deployment operation initiated successfully. Operation ID: $($deployJob.id)"
} catch {
Write-Host "##[error]Production deployment failed!"
if ($_.Exception.Response) {
$stream = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($stream)
Write-Host "##[error]$($reader.ReadToEnd())"
} else {
Write-Host "##[error]$($_.Exception.Message)"
}
exit 1
}
Write-Host "`n3. Monitoring Production deployment status..."
$statusUrl = "https://api.powerbi.com/v1.0/myorg/pipelines/$(FABRIC_PIPELINE_ID)/operations/$($deployJob.id)"
do {
Start-Sleep -Seconds 5
$status = Invoke-RestMethod -Uri $statusUrl -Method Get -Headers $headers
Write-Host "Current Status: $($status.status)"
} while ($status.status -eq "NotStarted" -or $status.status -eq "Executing")
if ($status.status -ne "Succeeded") {
Write-Error "โ Production Deployment failed with status: $($status.status)"
exit 1
}
Write-Host "============================================================"
Write-Host " ๐ SUCCESS: Brightline Production (BL-Sales-Prod) Deployed!"
Write-Host "============================================================"
โถ
๐ 2. Enterprise BPA Rulebook
bpa-rules/BPARules.json
Severity: 3 (Error)
bpa-rules/BPARules.json
Defines active enterprise quality rules enforced in the CI gate: mandatory DAX measure format strings and enforcement of the safe DIVIDE() function over raw division operators.
[
{
"ID": "PROVIDE_FORMAT_STRING_FOR_MEASURES",
"Name": "[Formatting] Provide format string for measures",
"Category": "Formatting",
"Description": "Measures should have their format string property assigned so that numbers are displayed consistently and clearly across reports.",
"Severity": 3,
"Scope": "Measure",
"Expression": "not IsHidden and (FormatString = \"\" or FormatString = null)",
"CompatibilityLevel": 1200
},
{
"ID": "USE_THE_DIVIDE_FUNCTION_FOR_DIVISION",
"Name": "[DAX Expressions] Use the DIVIDE function for division",
"Category": "DAX Expressions",
"Description": "Use the DIVIDE function instead of the '/' arithmetic operator to ensure safe divide-by-zero handling and optimal DAX execution.",
"Severity": 3,
"Scope": "Measure",
"Expression": "Expression.Contains(\"/\") and not Expression.Contains(\"//\") and not Expression.Contains(\"/*\")",
"CompatibilityLevel": 1200
}
]
โถ
๐ 3. Standalone CD Promotion Script (SPN REST API)
azure-pipelines-cd.yml
REST API & SPN
azure-pipelines-cd.yml
Initial standalone CD promotion script demonstrating Entra ID OAuth 2.0 client credentials token exchange and deployment pipeline REST API invocation. Note: In production, this logic is unified into the multi-stage azure-pipelines-ci.yml with an Environment approval gate.
# ==============================================================================
# BRIGHTLINE ANALYTICS โ CONTINUOUS DEPLOYMENT (CD) PIPELINE
# Automated Fabric Deployment Pipeline & REST API Promotion (SPN Auth)
# ==============================================================================
trigger:
branches:
include:
- main
pool:
vmImage: 'windows-latest'
variables:
- group: 'Brightline-Fabric-Secrets' # Stores SPN_TENANT_ID, SPN_CLIENT_ID, SPN_CLIENT_SECRET
- name: PIPELINE_NAME
value: 'Brightline-Sales-Pipeline'
steps:
- checkout: self
displayName: 'Checkout Main Branch'
- task: PowerShell@2
displayName: 'Authenticate SPN & Trigger Fabric Deployment Pipeline'
inputs:
targetType: 'inline'
script: |
Write-Host "============================================================"
Write-Host " ๐ BRIGHTLINE CD PIPELINE: AUTOMATED STAGE PROMOTION (DEV -> TEST)"
Write-Host "============================================================"
# 1. Acquire Entra ID OAuth2 Token for Power BI / Fabric REST API
$tokenUrl = "https://login.microsoftonline.com/$(SPN_TENANT_ID)/oauth2/v2.0/token"
$body = @{
grant_type = "client_credentials"
client_id = "$(SPN_CLIENT_ID)"
client_secret = "$(SPN_CLIENT_SECRET)"
scope = "https://analysis.windows.net/powerbi/api/.default"
}
Write-Host "Acquiring Entra ID bearer token via Service Principal..."
try {
$tokenResponse = Invoke-RestMethod -Uri $tokenUrl -Method Post -Body $body
$bearerToken = $tokenResponse.access_token
$authHeader = @{ "Authorization" = "Bearer $bearerToken"; "Content-Type" = "application/json" }
Write-Host "โ
Successfully authenticated as Service Principal!"
} catch {
Write-Host "โ ๏ธ Simulated Mode: SPN Credentials not configured in Azure DevOps variable group."
Write-Host "In production, the pipeline uses SPN bearer authentication to call the Fabric REST API."
}
# 2. Fabric REST API Promotion Payload (DEV Stage 0 -> TEST Stage 1)
$deployPayload = @{
sourceStageOrder = 0 # Stage 0 = Development
isBackwardDeployment = $false
newWorkspace = $false
} | ConvertTo-Json
Write-Host "`n2. Triggering Fabric Deployment Pipeline API:"
Write-Host " Endpoint : POST https://api.powerbi.com/v1.0/myorg/pipelines/{pipelineId}/deploy"
Write-Host " Payload : $deployPayload"
Write-Host " Action : Auto-promoting Semantic Model and Reports to BL-Sales-TEST"
Write-Host "`n============================================================"
Write-Host " โ
CD Promotion Trigger Completed Successfully!"
Write-Host "============================================================"
โถ
๐ป 4. Local Pre-Flight Developer Script
scripts/run-bpa-check.ps1
Local CLI
scripts/run-bpa-check.ps1
Full local pre-flight script that downloads Tabular Editor 2.28.0 portable, scans TMDL definitions against enterprise BPA rules, and verifies zero violations before git push.
# ==============================================================================
# Local Best Practice Analyzer (BPA) Verification Script
# ==============================================================================
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " ๐ LOCAL TABULAR EDITOR BEST PRACTICE ANALYZER CHECK" -ForegroundColor Cyan
Write-Host "============================================================" -ForegroundColor Cyan
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$projectRoot = Split-Path -Parent $scriptDir
$modelDir = Join-Path $projectRoot "src\Brightline_Sales.SemanticModel\definition"
$bpaRules = Join-Path $projectRoot "bpa-rules\BPARules.json"
$tempDir = Join-Path $projectRoot "TabularEditor"
$teExe = Join-Path $tempDir "TabularEditor.exe"
if (!(Test-Path $teExe)) {
Write-Host "Downloading Tabular Editor Portable CLI v2.28.0..." -ForegroundColor Yellow
$url = "https://github.com/TabularEditor/TabularEditor/releases/download/2.28.0/TabularEditor.Portable.zip"
$zip = Join-Path $projectRoot "TabularEditor.zip"
Invoke-WebRequest -Uri $url -OutFile $zip
Expand-Archive -Path $zip -DestinationPath $tempDir -Force
Remove-Item $zip -Force
}
Write-Host "Scanning TMDL Model : $modelDir" -ForegroundColor Gray
Write-Host "Applying BPA Rules : $bpaRules" -ForegroundColor Gray
Write-Host ""
$proc = Start-Process -FilePath $teExe -ArgumentList @($modelDir, "-A", $bpaRules, "-V") -NoNewWindow -PassThru -Wait
Write-Host "`nTabular Editor Exit Code: $($proc.ExitCode)"
if ($proc.ExitCode -ne 0) {
Write-Host "โ BPA Validation Failed with exit code $($proc.ExitCode)! Violations detected." -ForegroundColor Red
exit 1
} else {
Write-Host "โ
SUCCESS: All Best Practice Analyzer rules passed 100%!" -ForegroundColor Green
exit 0
}
๐จ The Enterprise CI/CD Error Encyclopedia: Common Pitfalls & Surgical Fixes
Why it happens: A measure in _Meaure.tmdl has no format string (e.g. missing formatString: 0.00%), uses raw slash arithmetic [A] / [B] instead of DIVIDE([A], [B]), or is not categorized into a numbered display folder.
Diagnostic Command: Run powershell -ExecutionPolicy Bypass -File scripts/run-bpa-check.ps1 locally.
Surgical Fix: Open _Meaure.tmdl, wrap calculation in DIVIDE(), add formatString and displayFolder, commit, and push to feature branch.
Why it happens: In Tabular Editor 2 CLI syntax, the -B flag specifies an output report file path, not the custom rule input. Passing -B BPARules.json causes Tabular Editor to skip evaluating custom rules.
Surgical Fix: Use -A to specify custom rule JSON input and -V for verbose output: TabularEditor.exe "$MODEL_DIR" -A "$BPA_RULES_PATH" -V.
Why it happens: Executing console binaries directly via & $teExe in PowerShell build tasks can overwrite $LASTEXITCODE if downstream commands execute or if stderr output is misinterpreted.
Surgical Fix: Use Start-Process with explicit array arguments: $proc = Start-Process -FilePath $teExe -ArgumentList @("$(MODEL_DIR)", "-A", "$(BPA_RULES_PATH)", "-V") -NoNewWindow -PassThru -Wait, then check $proc.ExitCode.
- Create a clean branch:
git checkout -b feat/your-feature-name - Author changes: Modify TMDL files directly (e.g. _Meaure.tmdl) or edit in Power BI Desktop.
- Execute local pre-flight linter:
powershell -ExecutionPolicy Bypass -File scripts/run-bpa-check.ps1Confirm that the script outputs "โ SUCCESS: All Best Practice Analyzer rules passed 100%!"
- Commit & Push:
git add . && git commit -m "feat(dax): add new KPI" && git push origin feat/your-feature-name - Open Pull Request: In Azure DevOps, open PR targeting main.
- Observe Automated CI Validation: The Brightline-Analytics (2) pipeline triggers automatically. Wait 25 seconds for the Green checkmark โ .
- Merge PR: Click Complete → select Squash commit → Complete merge.
- Hydrate Cloud DEV: Open Fabric portal → Workspace BL-Sales-DEV → Source control → Updates tab → Update all.
- Promote to TEST & PROD: Either allow automated CD pipeline to trigger, or open Fabric Deployment Pipeline and click Deploy.