<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mintu Ghosh</title>
    <description>The latest articles on DEV Community by Mintu Ghosh (@vedaforge_eng).</description>
    <link>https://dev.to/vedaforge_eng</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4076575%2F23946cfa-c2e3-42d3-bc38-0ece462d54f2.jpg</url>
      <title>DEV Community: Mintu Ghosh</title>
      <link>https://dev.to/vedaforge_eng</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vedaforge_eng"/>
    <language>en</language>
    <item>
      <title>The empty folder that would have emptied a production workspace</title>
      <dc:creator>Mintu Ghosh</dc:creator>
      <pubDate>Fri, 21 Aug 2026 09:08:18 +0000</pubDate>
      <link>https://dev.to/vedaforge_eng/the-empty-folder-that-would-have-emptied-a-production-workspace-31oj</link>
      <guid>https://dev.to/vedaforge_eng/the-empty-folder-that-would-have-emptied-a-production-workspace-31oj</guid>
      <description>&lt;p&gt;Deployment tools that support cleanup usually work out what to remove by&lt;br&gt;
comparison: anything present in the target but absent from the source is an&lt;br&gt;
orphan, so delete it.&lt;/p&gt;

&lt;p&gt;That rule is fine. It has one input state where it becomes destructive, and that&lt;br&gt;
state is completely normal.&lt;/p&gt;
&lt;h2&gt;
  
  
  The state
&lt;/h2&gt;

&lt;p&gt;An empty source folder.&lt;/p&gt;

&lt;p&gt;If the source has no items, then &lt;em&gt;everything&lt;/em&gt; in the target is absent from the&lt;br&gt;
source. Every item in the target workspace is an orphan. Cleanup removes all of&lt;br&gt;
it, reports success, and the deployment then publishes nothing over the top.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why you will hit it
&lt;/h2&gt;

&lt;p&gt;Not through carelessness. Through sequence.&lt;/p&gt;

&lt;p&gt;Onboarding a new solution goes: create the folder, wire up the pipeline, connect&lt;br&gt;
the source workspace, commit the items. Between step one and step four the&lt;br&gt;
folder exists and is empty.&lt;/p&gt;

&lt;p&gt;We were sitting in exactly that state across two of three solutions when I&lt;br&gt;
noticed. The folders existed. Each contained one auto-generated placeholder file&lt;br&gt;
and no actual items.&lt;/p&gt;

&lt;p&gt;If anything had promoted to those environments in that window, cleanup would&lt;br&gt;
have run against a populated target with an empty source.&lt;/p&gt;
&lt;h2&gt;
  
  
  The guard
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assert_source_not_empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;solution&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;item_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;count_fabric_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;item_count&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;read_bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALLOW_EMPTY_SOURCE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WARNING: {} contains no items, but ALLOW_EMPTY_SOURCE is set.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Solution &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; has no items under {}.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Refusing to deploy to {} because orphan removal would treat every &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;item in the target workspace as an orphan and delete it.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Connect the source workspace and commit its items first.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;solution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Three things I would keep if I wrote it again:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It runs before any API call.&lt;/strong&gt; Not before cleanup — before the SDK is even&lt;br&gt;
loaded. Nothing has authenticated, nothing has been resolved, nothing can&lt;br&gt;
partially happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The error says what would have happened&lt;/strong&gt;, not just what is missing. "No items&lt;br&gt;
found" invites someone to force it through. "Would delete everything in the&lt;br&gt;
target" does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is an override&lt;/strong&gt;, because emptying a workspace is occasionally what you&lt;br&gt;
want. It is explicit, it is not the default, and it logs loudly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Testing the dangerous thing on a laptop
&lt;/h2&gt;

&lt;p&gt;This is the one piece of logic whose failure mode is destructive, so I wanted it&lt;br&gt;
provable without touching a real workspace.&lt;/p&gt;

&lt;p&gt;The cloud SDK imports were moved behind a function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_load_fabric_sdk&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;azure.identity&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ClientSecretCredential&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fabric_cicd&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FabricWorkspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;publish_all_items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{...}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything up to and including the guard is plain path handling. So the tests&lt;br&gt;
run with neither &lt;code&gt;fabric-cicd&lt;/code&gt; nor &lt;code&gt;azure-identity&lt;/code&gt; installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_empty_solution_folder_aborts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;empty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Readme.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;placeholder&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;caught&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_source_not_empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UAT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;caught&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the placeholder file in that test. An empty-looking folder is rarely&lt;br&gt;
literally empty — it usually has a README or a &lt;code&gt;.gitkeep&lt;/code&gt;. The count has to be&lt;br&gt;
of real items, not of files, or the guard passes when it should not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general question
&lt;/h2&gt;

&lt;p&gt;For any tool that deletes by comparison: what happens when one side of the&lt;br&gt;
comparison is empty?&lt;/p&gt;

&lt;p&gt;If the answer is "delete everything", decide whether that state is reachable in&lt;br&gt;
normal use. In our case it was not just reachable, it was a required step in&lt;br&gt;
onboarding.&lt;/p&gt;




&lt;p&gt;Full context: [&lt;a href="https://www.linkedin.com/pulse/enterprise-microsoft-fabric-cicd-selective-one-azure-devops-ghosh-uxzsf/" rel="noopener noreferrer"&gt;https://www.linkedin.com/pulse/enterprise-microsoft-fabric-cicd-selective-one-azure-devops-ghosh-uxzsf/&lt;/a&gt;] &lt;br&gt;
Code: [&lt;a href="https://github.com/VEDAFORGE/fabric-cicd-reference/tree/v2.0.0" rel="noopener noreferrer"&gt;https://github.com/VEDAFORGE/fabric-cicd-reference/tree/v2.0.0&lt;/a&gt;]&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>microsoftfabric</category>
      <category>python</category>
    </item>
    <item>
      <title>A green pipeline that deployed nothing</title>
      <dc:creator>Mintu Ghosh</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:55:52 +0000</pubDate>
      <link>https://dev.to/vedaforge_eng/a-green-pipeline-that-deployed-nothing-2npj</link>
      <guid>https://dev.to/vedaforge_eng/a-green-pipeline-that-deployed-nothing-2npj</guid>
      <description>&lt;p&gt;The worst CI outcome is not a failing build. It is a passing build that did&lt;br&gt;
nothing and looked correct.&lt;/p&gt;

&lt;p&gt;Here is one way to get there, and it takes a single missing line of YAML.&lt;/p&gt;
&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;A pipeline that deploys only what changed. It diffs against the parent commit,&lt;br&gt;
maps changed paths onto folders, and conditions each deployment stage on the&lt;br&gt;
result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_git&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;diff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--name-only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;repo_root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The failure
&lt;/h2&gt;

&lt;p&gt;Azure DevOps can check out with shallow fetch. A shallow clone has no parent&lt;br&gt;
commit.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;git diff HEAD~1 HEAD&lt;/code&gt; does not error. It returns &lt;strong&gt;nothing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Nothing changed, therefore nothing needs deploying, therefore every stage skips,&lt;br&gt;
therefore the pipeline is green. Nobody is alerted, because from the outside&lt;br&gt;
this is exactly what a run looks like when someone edits a README.&lt;/p&gt;

&lt;p&gt;You find out later, when something that was supposed to be in production is not.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix is two parts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Make the pipeline correct:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;checkout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self&lt;/span&gt;
  &lt;span class="na"&gt;fetchDepth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;And make the script refuse to guess:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;run_git&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rev-parse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--is-shallow-repository&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;repo_root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;GitError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This is a shallow clone, so there is no history to diff against.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Set &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fetchDepth: 0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; on the checkout step, or disable shallow fetch &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in the pipeline settings.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second part matters more than the first. &lt;code&gt;fetchDepth: 0&lt;/code&gt; is one line in a&lt;br&gt;
YAML file that someone will eventually remove while tidying up, or that will be&lt;br&gt;
absent when a colleague copies the pipeline into a new project. The check turns&lt;br&gt;
that into a red build with an instruction attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  And a test, because the line is deletable
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_detection_stage_checks_out_full_history&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;checkout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;detect_stage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;steps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;self&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetchDepth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;change detection needs full history; fetchDepth must be 0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It parses the pipeline YAML and asserts the line is still there. Slightly&lt;br&gt;
unusual to test your own CI config, but this is a config value with a silent&lt;br&gt;
failure mode, which is exactly the kind worth pinning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general shape
&lt;/h2&gt;

&lt;p&gt;Ask of any conditional deployment: &lt;strong&gt;what does this do when its input is&lt;br&gt;
missing rather than wrong?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Wrong input usually errors. Missing input often produces an empty result that&lt;br&gt;
looks like a legitimate negative. Empty and "nothing to do" are the same value,&lt;br&gt;
and only one of them is safe.&lt;/p&gt;

&lt;p&gt;Anywhere those two are indistinguishable, make the code refuse rather than&lt;br&gt;
assume.&lt;/p&gt;




&lt;p&gt;Full context: [&lt;a href="https://www.linkedin.com/pulse/enterprise-microsoft-fabric-cicd-selective-one-azure-devops-ghosh-uxzsf" rel="noopener noreferrer"&gt;https://www.linkedin.com/pulse/enterprise-microsoft-fabric-cicd-selective-one-azure-devops-ghosh-uxzsf&lt;/a&gt;]&lt;br&gt;
Code: [&lt;a href="https://github.com/VEDAFORGE/fabric-cicd-reference/tree/v2.0.0" rel="noopener noreferrer"&gt;https://github.com/VEDAFORGE/fabric-cicd-reference/tree/v2.0.0&lt;/a&gt;]&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>git</category>
      <category>azuredevops</category>
      <category>devops</category>
    </item>
    <item>
      <title>Azure DevOps stage output variables - three things that silently do not work</title>
      <dc:creator>Mintu Ghosh</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:53:32 +0000</pubDate>
      <link>https://dev.to/vedaforge_eng/azure-devops-stage-output-variables-three-things-that-silently-do-not-work-fl3</link>
      <guid>https://dev.to/vedaforge_eng/azure-devops-stage-output-variables-three-things-that-silently-do-not-work-fl3</guid>
      <description>&lt;p&gt;I needed one stage to compute something and later stages to condition on it.&lt;br&gt;
Azure DevOps supports this. It also fails silently in three specific ways, and&lt;br&gt;
none of them produce an error message that points at the cause.&lt;/p&gt;

&lt;p&gt;Nothing Fabric-specific here. This applies to any multi-stage pipeline.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting the variable
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DetectChanges&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Detect&lt;/span&gt;
      &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python scripts/detect_changes.py&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detect&lt;/span&gt;          &lt;span class="c1"&gt;# &amp;lt;- required&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The script writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;##vso[task.setvariable variable=ved;isOutput=true]true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Reading it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy_ved&lt;/span&gt;
  &lt;span class="na"&gt;dependsOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DetectChanges&lt;/span&gt;
  &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
    &lt;span class="s"&gt;eq(dependencies.DetectChanges.outputs['Detect.detect.ved'], 'true')&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Gotcha 1: the values are strings
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;eq(dependencies.DetectChanges.outputs['Detect.detect.ved'], &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# never matches&lt;/span&gt;
&lt;span class="s"&gt;eq(dependencies.DetectChanges.outputs['Detect.detect.ved'], 'true')&lt;/span&gt;  &lt;span class="c1"&gt;# correct&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first one is valid YAML, valid expression syntax, and always false. Your&lt;br&gt;
stage skips every time and the pipeline goes green because a skipped stage is&lt;br&gt;
not a failed stage.&lt;/p&gt;

&lt;p&gt;Quote the value.&lt;/p&gt;
&lt;h2&gt;
  
  
  Gotcha 2: the reference has three parts, not one
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dependencies.&amp;lt;Stage&amp;gt;.outputs['&amp;lt;Job&amp;gt;.&amp;lt;StepName&amp;gt;.&amp;lt;Variable&amp;gt;']
                              ^^^^^ ^^^^^^^^^^ ^^^^^^^^^^
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The job name and the step &lt;code&gt;name&lt;/code&gt; both appear in the string. Miss either and you&lt;br&gt;
get an empty value rather than an error.&lt;/p&gt;

&lt;p&gt;The step &lt;code&gt;name&lt;/code&gt; is not &lt;code&gt;displayName&lt;/code&gt;. If your step only has a &lt;code&gt;displayName&lt;/code&gt;, it&lt;br&gt;
has no addressable name and the variable is unreachable.&lt;/p&gt;
&lt;h2&gt;
  
  
  Gotcha 3: the consuming stage must declare the dependency
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy_ved&lt;/span&gt;
  &lt;span class="na"&gt;dependsOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DetectChanges&lt;/span&gt;      &lt;span class="c1"&gt;# without this the variable cannot resolve&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;dependencies.X&lt;/code&gt; only sees stages this stage actually depends on. Without&lt;br&gt;
&lt;code&gt;dependsOn&lt;/code&gt;, the expression resolves to nothing. No error, no warning, and the&lt;br&gt;
condition evaluates false.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why all three fail the same way
&lt;/h2&gt;

&lt;p&gt;Every one of these produces an empty string, which compares false, which skips&lt;br&gt;
the stage, which is a legitimate outcome that Azure DevOps reports as success.&lt;/p&gt;

&lt;p&gt;You get a green pipeline that did nothing. That is worse than a red one, because&lt;br&gt;
nothing prompts you to look.&lt;/p&gt;
&lt;h2&gt;
  
  
  Debugging it
&lt;/h2&gt;

&lt;p&gt;Print what you set, in the producing job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "ved=$(detect.ved)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And publish the decision as a build artifact. I write a JSON file from the&lt;br&gt;
detection script and publish it, so every run has a durable record of what it&lt;br&gt;
decided rather than a log line that ages out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(Build.ArtifactStagingDirectory)/change-detection.json&lt;/span&gt;
  &lt;span class="na"&gt;artifact&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;change-detection&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The one that bit hardest
&lt;/h2&gt;

&lt;p&gt;Not in the list above. A stage skipping is indistinguishable, in the UI, from a&lt;br&gt;
stage that was correctly filtered out.&lt;/p&gt;

&lt;p&gt;So when you are looking at a run and thinking "good, the unrelated stages were&lt;br&gt;
skipped", check &lt;em&gt;why&lt;/em&gt; they skipped. Branch condition and output-variable&lt;br&gt;
condition look identical in the stage view and mean completely different things.&lt;/p&gt;

&lt;p&gt;I nearly used a screenshot as evidence for the wrong claim on exactly that&lt;br&gt;
basis.&lt;/p&gt;




&lt;p&gt;Full context: [&lt;a href="https://www.linkedin.com/pulse/enterprise-microsoft-fabric-cicd-selective-one-azure-devops-ghosh-uxzsf" rel="noopener noreferrer"&gt;https://www.linkedin.com/pulse/enterprise-microsoft-fabric-cicd-selective-one-azure-devops-ghosh-uxzsf&lt;/a&gt;]&lt;br&gt;
Code: [&lt;a href="https://github.com/VEDAFORGE/fabric-cicd-reference/tree/v2.0.0" rel="noopener noreferrer"&gt;https://github.com/VEDAFORGE/fabric-cicd-reference/tree/v2.0.0&lt;/a&gt;]&lt;/p&gt;

</description>
      <category>azuredevops</category>
      <category>cicd</category>
      <category>yaml</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why I left Warehouse out of our Fabric deployment scope</title>
      <dc:creator>Mintu Ghosh</dc:creator>
      <pubDate>Mon, 17 Aug 2026 15:01:05 +0000</pubDate>
      <link>https://dev.to/vedaforge_eng/why-i-left-warehouse-out-of-our-fabric-deployment-scope-2jca</link>
      <guid>https://dev.to/vedaforge_eng/why-i-left-warehouse-out-of-our-fabric-deployment-scope-2jca</guid>
      <description>&lt;p&gt;title: Why I left Warehouse out of our Fabric deployment scope&lt;br&gt;
published: true&lt;br&gt;
tags: microsoftfabric, datawarehouse, cicd, devops&lt;/p&gt;

&lt;p&gt;Our Fabric deployment pipeline handles sixteen item types. Warehouse is not one&lt;br&gt;
of them, and that was deliberate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;DEFAULT_ITEM_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DataPipeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lakehouse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Notebook&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SemanticModel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# "Warehouse" is intentionally excluded. Warehouse schema deployment must
&lt;/span&gt;    &lt;span class="c1"&gt;# be handled separately to avoid schema reset risk during publish.
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Environment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Eventhouse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The reason
&lt;/h2&gt;

&lt;p&gt;Publishing a warehouse through this path can reset its schema.&lt;/p&gt;

&lt;p&gt;Not "might behave unexpectedly". The failure mode is that a deployment intended&lt;br&gt;
to be additive removes structure, and the thing that removes it is the same&lt;br&gt;
routine that successfully deploys the other sixteen types.&lt;/p&gt;
&lt;h2&gt;
  
  
  The choice that follows
&lt;/h2&gt;

&lt;p&gt;Two options once you know that.&lt;/p&gt;

&lt;p&gt;Include it and hope nobody deploys a warehouse without reading the docs. The&lt;br&gt;
pipeline supports everything, and one day someone promotes a change on a Friday&lt;br&gt;
and finds out.&lt;/p&gt;

&lt;p&gt;Or exclude it, document why, and handle warehouse deployment as its own problem&lt;br&gt;
with its own tooling.&lt;/p&gt;

&lt;p&gt;I took the second. An automation that covers most cases and silently corrupts&lt;br&gt;
the rest is worse than one that covers most cases and refuses the rest. The&lt;br&gt;
refusal is visible. The corruption is not.&lt;/p&gt;
&lt;h2&gt;
  
  
  Making the exclusion loud
&lt;/h2&gt;

&lt;p&gt;An exclusion is only useful if someone notices it. Three things help:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The comment sits inside the list&lt;/strong&gt;, not in a doc nobody opens. Anyone reading&lt;br&gt;
the item types sees the gap and the reason in the same glance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is in the README&lt;/strong&gt; under known limitations, next to the other things the&lt;br&gt;
framework does not do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is a test.&lt;/strong&gt; It asserts Warehouse is absent from the default scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_warehouse_stays_excluded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Warehouse publish can reset schema, so it is handled separately.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertNotIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Warehouse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEFAULT_ITEM_TYPES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That test looks silly. It is asserting that a string is missing from a list.&lt;/p&gt;

&lt;p&gt;It exists because the natural instinct, when someone hits "my warehouse did not&lt;br&gt;
deploy", is to add the string to the list. The test turns that into a failing&lt;br&gt;
build with an explanation attached, instead of a merged one-line change that&lt;br&gt;
nobody reviews closely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would want next
&lt;/h2&gt;

&lt;p&gt;The honest gap: excluding it does not solve warehouse deployment, it defers it.&lt;br&gt;
Teams that need warehouse promotion still need an answer, and mine is currently&lt;br&gt;
"do it separately, carefully".&lt;/p&gt;

&lt;p&gt;That is a real limitation rather than a design choice, and worth saying so.&lt;/p&gt;

&lt;p&gt;Full context: [&lt;a href="https://www.linkedin.com/pulse/building-enterprise-microsoft-fabric-cicd-practical-guide-mintu-ghosh-f3dbf/" rel="noopener noreferrer"&gt;https://www.linkedin.com/pulse/building-enterprise-microsoft-fabric-cicd-practical-guide-mintu-ghosh-f3dbf/&lt;/a&gt;] &lt;br&gt;
Code: [&lt;a href="https://github.com/vedaforge-team/fabric-cicd-reference/tree/v1.0.0" rel="noopener noreferrer"&gt;https://github.com/vedaforge-team/fabric-cicd-reference/tree/v1.0.0&lt;/a&gt;]&lt;/p&gt;

</description>
      <category>microsoftfabric</category>
      <category>datawarehouse</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>The refactor that made every validation build pass without validating anything</title>
      <dc:creator>Mintu Ghosh</dc:creator>
      <pubDate>Sat, 15 Aug 2026 10:45:19 +0000</pubDate>
      <link>https://dev.to/vedaforge_eng/the-refactor-that-made-every-validation-build-pass-without-validating-anything-50gd</link>
      <guid>https://dev.to/vedaforge_eng/the-refactor-that-made-every-validation-build-pass-without-validating-anything-50gd</guid>
      <description>&lt;p&gt;Short one. A bug I introduced, caught by reading the diff rather than by any&lt;br&gt;
test, and the reason no test could have caught it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;A deployment script with a validation mode. On the &lt;code&gt;develop&lt;/code&gt; branch it runs with&lt;br&gt;
&lt;code&gt;VALIDATE_ONLY=true&lt;/code&gt; and is supposed to check everything without writing&lt;br&gt;
anything.&lt;/p&gt;

&lt;p&gt;Original order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;workspace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_workspace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="c1"&gt;# authenticate, resolve, parse
&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;read_bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VALIDATE_ONLY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Validation-only mode. No workspace changes applied.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;

&lt;span class="nf"&gt;publish_all_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I did
&lt;/h2&gt;

&lt;p&gt;I was restructuring the function and moved things around for readability. The&lt;br&gt;
result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;read_bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VALIDATE_ONLY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Validation-only mode. No workspace changes applied.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;

&lt;span class="n"&gt;workspace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_workspace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="c1"&gt;# now unreachable in validation mode
&lt;/span&gt;&lt;span class="nf"&gt;publish_all_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks tidier. Nothing writes to a workspace in either version. Every test passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it is broken
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;build_workspace()&lt;/code&gt; is not setup code. It is the validation.&lt;/p&gt;

&lt;p&gt;Constructing that object authenticates the service principal, resolves the&lt;br&gt;
target workspace over the API, and parses every item in the repository. Most of&lt;br&gt;
what can be wrong is discovered right there.&lt;/p&gt;

&lt;p&gt;Return before it and the validation stage prints a reassuring message, exits&lt;br&gt;
zero, and has checked nothing. Every build on the validation branch goes green.&lt;br&gt;
Broken items reach the release branch unnoticed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the tests did not catch it
&lt;/h2&gt;

&lt;p&gt;Every test asserted what the code &lt;em&gt;did&lt;/em&gt;: it did not publish, it did not remove&lt;br&gt;
orphans, it exited cleanly. All still true.&lt;/p&gt;

&lt;p&gt;Nothing asserted that validation actually validates. There was no test for&lt;br&gt;
"the workspace object gets built before the early return", because that reads&lt;br&gt;
like testing an implementation detail right up until the moment it is the entire&lt;br&gt;
safety property.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I changed
&lt;/h2&gt;

&lt;p&gt;Restored the order, and left a comment explaining why the line is where it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Built before the validate-only check on purpose. Constructing the
# FabricWorkspace authenticates, resolves the target workspace and parses
# every item, so it is the part that actually validates. Returning before
# this would make develop builds green without checking anything.
&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_workspace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A comment is a weak guard. But the alternative — a test that asserts an&lt;br&gt;
authentication call happens in a mode defined by not doing anything — is awkward&lt;br&gt;
enough that I would rather the next person read one sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general version
&lt;/h2&gt;

&lt;p&gt;A refactor can hollow out a safety check while the whole suite stays green, when&lt;br&gt;
the tests assert absence of effects rather than presence of the check.&lt;/p&gt;

&lt;p&gt;If you have a validate-only or dry-run mode, ask what in it is actually doing&lt;br&gt;
the validating, and whether anything would notice if that stopped happening.&lt;/p&gt;




&lt;p&gt;Full context: [&lt;a href="https://www.linkedin.com/pulse/building-enterprise-microsoft-fabric-cicd-practical-guide-mintu-ghosh-f3dbf/" rel="noopener noreferrer"&gt;https://www.linkedin.com/pulse/building-enterprise-microsoft-fabric-cicd-practical-guide-mintu-ghosh-f3dbf/&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;Code: [&lt;a href="https://github.com/vedaforge-team/fabric-cicd-reference/tree/v1.0.0" rel="noopener noreferrer"&gt;https://github.com/vedaforge-team/fabric-cicd-reference/tree/v1.0.0&lt;/a&gt;]&lt;/p&gt;

</description>
      <category>python</category>
      <category>cicd</category>
      <category>testing</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your Fabric deployment failed and it is probably not your code</title>
      <dc:creator>Mintu Ghosh</dc:creator>
      <pubDate>Sat, 15 Aug 2026 10:42:09 +0000</pubDate>
      <link>https://dev.to/vedaforge_eng/your-fabric-deployment-failed-and-it-is-probably-not-your-code-1hi2</link>
      <guid>https://dev.to/vedaforge_eng/your-fabric-deployment-failed-and-it-is-probably-not-your-code-1hi2</guid>
      <description>&lt;p&gt;I wrote a deployment script for Microsoft Fabric. It is a thin wrapper around&lt;br&gt;
the &lt;code&gt;fabric-cicd&lt;/code&gt; library. Maybe 150 lines with the comments.&lt;/p&gt;

&lt;p&gt;Then I wrote down everything that has to be true before it works. That list came&lt;br&gt;
to 43 items.&lt;/p&gt;

&lt;p&gt;That ratio is the thing worth knowing before you start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The breakdown
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Checks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fabric environment&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity and security&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure DevOps configuration&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment configuration&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment validation&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-deployment verification&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Identity is the largest, and three of those ten are the same check repeated once&lt;br&gt;
per environment: &lt;strong&gt;is the service principal actually a member of this&lt;br&gt;
workspace.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DEV, UAT and PROD, separately. Adding it to one does nothing for the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is annoying specifically
&lt;/h2&gt;

&lt;p&gt;Nothing in your repository indicates a permission is missing. The code is&lt;br&gt;
byte-identical whether the service principal has access to all three workspaces&lt;br&gt;
or none of them.&lt;/p&gt;

&lt;p&gt;So the failure surfaces at the deployment step, which is the last thing in the&lt;br&gt;
chain and the first thing you look at. The error is real. The deployment did&lt;br&gt;
fail. Your code is fine.&lt;/p&gt;

&lt;p&gt;You start debugging in the wrong place by default, and the stack trace does not&lt;br&gt;
help because there is nothing wrong with the stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two of them are not settings
&lt;/h2&gt;

&lt;p&gt;Two items on the list are architectural decisions wearing checkbox clothing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only the Development workspace is connected to Git&lt;/li&gt;
&lt;li&gt;UAT and PROD are deployment targets only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Connect UAT to Git as well and that environment now has two sources of truth:&lt;br&gt;
the repository, and whoever last edited the workspace in the browser. Both look&lt;br&gt;
current. Nothing errors. You find out during an incident, when the thing you&lt;br&gt;
deployed is not the thing running.&lt;/p&gt;

&lt;p&gt;A checkbox is a strange home for a decision that consequential. It belongs there&lt;br&gt;
anyway, because it has to hold before anything downstream means anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical takeaway
&lt;/h2&gt;

&lt;p&gt;Check before you run, not after it fails. Diagnosing a missing prerequisite from&lt;br&gt;
a deployment error is slower than confirming it up front, every time.&lt;/p&gt;

&lt;p&gt;The full checklist is published here:&lt;br&gt;
&lt;code&gt;docs/checklists/DEPLOYMENT_READINESS_CHECKLIST.md&lt;/code&gt; in [&lt;a href="https://github.com/vedaforge-team/fabric-cicd-reference/tree/v1.0.0" rel="noopener noreferrer"&gt;https://github.com/vedaforge-team/fabric-cicd-reference/tree/v1.0.0&lt;/a&gt;].&lt;/p&gt;

&lt;p&gt;Of everything that implementation produced, it is the part I still use.&lt;/p&gt;




&lt;p&gt;Full walkthrough: [&lt;a href="https://www.linkedin.com/pulse/building-enterprise-microsoft-fabric-cicd-practical-guide-mintu-ghosh-f3dbf/" rel="noopener noreferrer"&gt;https://www.linkedin.com/pulse/building-enterprise-microsoft-fabric-cicd-practical-guide-mintu-ghosh-f3dbf/&lt;/a&gt;].&lt;/p&gt;

</description>
      <category>microsoftfabric</category>
      <category>azuredevops</category>
      <category>devops</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Deploy only what changed - selective deployment in an Azure DevOps monorepo</title>
      <dc:creator>Mintu Ghosh</dc:creator>
      <pubDate>Fri, 14 Aug 2026 09:47:36 +0000</pubDate>
      <link>https://dev.to/vedaforge_eng/deploy-only-what-changed-selective-deployment-in-an-azure-devops-monorepo-4l63</link>
      <guid>https://dev.to/vedaforge_eng/deploy-only-what-changed-selective-deployment-in-an-azure-devops-monorepo-4l63</guid>
      <description>&lt;p&gt;Three teams share one repository. A commit touching one team's folder must not&lt;br&gt;
redeploy the other two.&lt;/p&gt;

&lt;p&gt;The obvious answer is trigger path filters. They do not work for this, and the&lt;br&gt;
reason is worth understanding before you build anything.&lt;/p&gt;

&lt;p&gt;This is Microsoft Fabric, but the mechanism is generic. If you have a monorepo&lt;br&gt;
and an Azure DevOps pipeline, it applies.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why path filters do not solve it
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;develop&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;teams/alpha&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# &amp;lt;- decides IF the pipeline runs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A trigger path filter answers one question: should this pipeline run at all. It&lt;br&gt;
cannot tell a later stage &lt;em&gt;which&lt;/em&gt; folder changed, because it produces no&lt;br&gt;
variable anything downstream can read.&lt;/p&gt;

&lt;p&gt;You could make one pipeline per team. Then every fix is made three times and&lt;br&gt;
they drift.&lt;/p&gt;
&lt;h2&gt;
  
  
  The actual constraint
&lt;/h2&gt;

&lt;p&gt;Azure DevOps builds its stage graph at &lt;strong&gt;compile time&lt;/strong&gt;. Change detection can&lt;br&gt;
only answer at &lt;strong&gt;run time&lt;/strong&gt;. Stages cannot be created dynamically.&lt;/p&gt;

&lt;p&gt;So generate every stage for every team, always, and let run-time detection&lt;br&gt;
decide which execute. Unaffected ones render as &lt;strong&gt;Skipped&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That turns out to be better than dynamic generation. A skipped stage is visible&lt;br&gt;
evidence the pipeline considered that team and chose not to deploy it. A graph&lt;br&gt;
that never mentions the team proves nothing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Choosing the diff baseline
&lt;/h2&gt;

&lt;p&gt;There is no single "previous commit" that is right everywhere. Derive it from&lt;br&gt;
the shape of HEAD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_git&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rev-list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--parents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;repo_root&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;parent_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;parent_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;merge commit (first parent)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;parent_count&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;~1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;single parent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root commit (no parent)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pleasing part: Azure DevOps PR builds check out a merge of source into&lt;br&gt;
target, so the first parent &lt;em&gt;is&lt;/em&gt; the target branch before the merge. The&lt;br&gt;
first-parent rule covers PR builds, merge-commit promotions and squash merges&lt;br&gt;
without special-casing any of them. I never needed the &lt;code&gt;System.PullRequest&lt;/code&gt;&lt;br&gt;
variables.&lt;/p&gt;

&lt;p&gt;Known gap, documented rather than engineered around: a multi-commit push is&lt;br&gt;
evaluated from &lt;code&gt;HEAD~1&lt;/code&gt;, so only the last commit is inspected. Promotion goes&lt;br&gt;
through PRs where the full change set is visible, so this only affects direct&lt;br&gt;
pushes to the validate-only branch.&lt;/p&gt;
&lt;h2&gt;
  
  
  The failure mode that looks like success
&lt;/h2&gt;

&lt;p&gt;A shallow clone has no parent to diff against. Git returns an &lt;strong&gt;empty diff&lt;/strong&gt;,&lt;br&gt;
not an error. Every deployment skips and the run stays green.&lt;/p&gt;

&lt;p&gt;That is a false negative that looks like a pass. Fail loudly instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;run_git&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rev-parse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--is-shallow-repository&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;repo_root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;GitError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This is a shallow clone, so there is no history to diff against.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Set &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fetchDepth: 0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; on the checkout step, or disable shallow fetch.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And on the detection job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;checkout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self&lt;/span&gt;
  &lt;span class="na"&gt;fetchDepth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have a test asserting the pipeline still sets &lt;code&gt;fetchDepth: 0&lt;/code&gt;, because it is&lt;br&gt;
the kind of line someone removes while tidying.&lt;/p&gt;
&lt;h2&gt;
  
  
  Publishing the result
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;solution&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;solutions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;solutions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;solution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;##vso[task.setvariable variable={};isOutput=true]{}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;solution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Consuming it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy_UAT_ved&lt;/span&gt;
  &lt;span class="na"&gt;dependsOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DetectChanges&lt;/span&gt;
  &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
    &lt;span class="s"&gt;and(&lt;/span&gt;
      &lt;span class="s"&gt;succeeded(),&lt;/span&gt;
      &lt;span class="s"&gt;eq(dependencies.DetectChanges.outputs['Detect.detect.ved'], 'true'),&lt;/span&gt;
      &lt;span class="s"&gt;eq(variables['Build.SourceBranch'], 'refs/heads/release')&lt;/span&gt;
    &lt;span class="s"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things people get wrong here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Values are strings.&lt;/strong&gt; &lt;code&gt;eq(..., true)&lt;/code&gt; never matches. It must be &lt;code&gt;'true'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The reference has three parts&lt;/strong&gt; — job name, step name, variable name:
&lt;code&gt;dependencies.&amp;lt;Stage&amp;gt;.outputs['&amp;lt;Job&amp;gt;.&amp;lt;step&amp;gt;.&amp;lt;var&amp;gt;']&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The consuming stage must &lt;code&gt;dependsOn&lt;/code&gt; the producing stage&lt;/strong&gt;, or the variable
cannot be resolved at all.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Two conditions, not one
&lt;/h2&gt;

&lt;p&gt;Every stage carries both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;detection&lt;/strong&gt; decides &lt;em&gt;which&lt;/em&gt; solutions changed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;branch&lt;/strong&gt; decides &lt;em&gt;where&lt;/em&gt; they may deploy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both must hold. Detection never widens the branch rule and the branch rule never&lt;br&gt;
widens detection.&lt;/p&gt;

&lt;p&gt;This matters when reading results. A stage shows "Skipped" for either reason,&lt;br&gt;
and they prove different things. On the production branch, every validate and UAT&lt;br&gt;
stage skips for branch reasons while solutions still deploy. That is promotion&lt;br&gt;
control, not isolation. Isolation is a stage skipping while a sibling at the same&lt;br&gt;
tier succeeds.&lt;/p&gt;

&lt;p&gt;I nearly captioned a screenshot wrong on exactly this point.&lt;/p&gt;
&lt;h2&gt;
  
  
  Resolving config without duplication
&lt;/h2&gt;

&lt;p&gt;Solution plus environment becomes a variable name at compile time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;TARGET_WORKSPACE_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ format('$({0}_{1}_WORKSPACE_ID)',&lt;/span&gt;
                        &lt;span class="s"&gt;upper(parameters.solution),&lt;/span&gt;
                        &lt;span class="s"&gt;upper(parameters.environment)) }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ved&lt;/code&gt; + &lt;code&gt;UAT&lt;/code&gt; expands to &lt;code&gt;$(VED_UAT_WORKSPACE_ID)&lt;/code&gt;, resolved from the stage's&lt;br&gt;
variable group at run time. The identifier never enters the repository.&lt;/p&gt;
&lt;h2&gt;
  
  
  Scope the deployment, and refuse an empty source
&lt;/h2&gt;

&lt;p&gt;Two guards worth having, both learned the hard way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reject the repository root.&lt;/strong&gt; A root-scoped deployment sweeps every team's&lt;br&gt;
artifacts into one target, and the cleanup that follows is evaluated against the&lt;br&gt;
wrong set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refuse an empty source folder.&lt;/strong&gt; Orphan removal treats "present in target,&lt;br&gt;
absent from source" as delete. An empty folder therefore means &lt;em&gt;everything in&lt;br&gt;
the target is an orphan&lt;/em&gt;. A folder created but not yet populated is a normal&lt;br&gt;
intermediate state, so it has to fail safely rather than proceed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item_count&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;read_bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALLOW_EMPTY_SOURCE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Solution has no items. Refusing to deploy: orphan removal would &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;treat every item in the target as an orphan and delete it.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Test it without touching the cloud
&lt;/h2&gt;

&lt;p&gt;The classifier is a pure function: changed paths in, decision out. That makes&lt;br&gt;
the whole matrix testable offline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_d_ved_and_hr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertAffected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fabric-workspaces/Ved/Notebook.Notebook/notebook-content.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fabric-workspaces/HR/Notebook.Notebook/notebook-content.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;70 tests, stdlib &lt;code&gt;unittest&lt;/code&gt;, no pytest, no cloud access, under a tenth of a&lt;br&gt;
second. The six scenarios that matter — one team, each other team, two teams,&lt;br&gt;
all three, docs-only — are verified before any pipeline runs.&lt;/p&gt;

&lt;p&gt;One test I had to rewrite: it asserted a folder held exactly four items. True&lt;br&gt;
when written, false the moment someone authored a fifth. Do not couple tests to&lt;br&gt;
live content; use fixtures for exact counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verified end to end
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Branch&lt;/th&gt;
&lt;th&gt;Detected&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;develop&lt;/td&gt;
&lt;td&gt;one solution, 2 files&lt;/td&gt;
&lt;td&gt;that one validated, other two skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;release&lt;/td&gt;
&lt;td&gt;one solution, 4 files&lt;/td&gt;
&lt;td&gt;that one deployed to UAT, other two skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;main&lt;/td&gt;
&lt;td&gt;all three, 39 files&lt;/td&gt;
&lt;td&gt;all three to PROD, all validate and UAT stages skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Edge case that works: item names containing spaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it still does not do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No approval gates&lt;/li&gt;
&lt;li&gt;Multi-commit pushes inspect only the last commit&lt;/li&gt;
&lt;li&gt;A shared framework change deploys nothing by design; there is a manual
override parameter for rolling one out deliberately&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Full walkthrough: [&lt;a href="https://www.linkedin.com/pulse/enterprise-microsoft-fabric-cicd-selective-one-azure-devops-ghosh-uxzsf/" rel="noopener noreferrer"&gt;https://www.linkedin.com/pulse/enterprise-microsoft-fabric-cicd-selective-one-azure-devops-ghosh-uxzsf/&lt;/a&gt;]. &lt;br&gt;
Code, including the test suite:&lt;br&gt;
[&lt;a href="https://github.com/vedaforge-team/fabric-cicd-reference/tree/v2.0.0" rel="noopener noreferrer"&gt;https://github.com/vedaforge-team/fabric-cicd-reference/tree/v2.0.0&lt;/a&gt;].&lt;/p&gt;

</description>
      <category>azuredevops</category>
      <category>cicd</category>
      <category>monorepo</category>
      <category>microsoftfabric</category>
    </item>
    <item>
      <title>Deploying Microsoft Fabric items from Azure DevOps with a service principal</title>
      <dc:creator>Mintu Ghosh</dc:creator>
      <pubDate>Thu, 13 Aug 2026 17:55:00 +0000</pubDate>
      <link>https://dev.to/vedaforge_eng/deploying-microsoft-fabric-items-from-azure-devops-with-a-service-principal-4nfa</link>
      <guid>https://dev.to/vedaforge_eng/deploying-microsoft-fabric-items-from-azure-devops-with-a-service-principal-4nfa</guid>
      <description>&lt;p&gt;Microsoft Fabric workspaces are edited in a browser. That works until you have&lt;br&gt;
more than one environment and need changes to move between them with a review&lt;br&gt;
step and a way back.&lt;/p&gt;

&lt;p&gt;This is the smallest thing that does that: one Python script, one Azure DevOps&lt;br&gt;
pipeline, three branches, three workspaces. Runs as a service principal, so no&lt;br&gt;
human session is involved.&lt;/p&gt;

&lt;p&gt;Full code: [&lt;a href="https://github.com/vedaforge-team/fabric-cicd-reference/tree/v1.0.0" rel="noopener noreferrer"&gt;https://github.com/vedaforge-team/fabric-cicd-reference/tree/v1.0.0&lt;/a&gt;]. &lt;br&gt;
This post covers the parts worth understanding&lt;br&gt;
before you copy it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The shape
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Development workspace  --(Fabric Git integration)--&amp;gt;  Azure DevOps repo
                                                            |
                                        develop  -&amp;gt;  validate against DEV
                                        release  -&amp;gt;  deploy to UAT
                                        main     -&amp;gt;  deploy to PROD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Only the Development workspace is Git-connected. UAT and PROD are reached over&lt;br&gt;
the Fabric REST API by the pipeline.&lt;/p&gt;

&lt;p&gt;Connect UAT to Git as well and that environment has two sources of truth: the&lt;br&gt;
repository, and whoever last edited the workspace directly. Both will look&lt;br&gt;
current. Nothing errors.&lt;/p&gt;
&lt;h2&gt;
  
  
  The script is a thin wrapper
&lt;/h2&gt;

&lt;p&gt;Everything environment-specific arrives as an environment variable. The pipeline&lt;br&gt;
supplies them from variable groups.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_workspace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;FabricWorkspace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;credential&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ClientSecretCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;read_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TENANT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;read_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;read_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CLIENT_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;FabricWorkspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;workspace_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;read_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TARGET_WORKSPACE_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;read_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TARGET_ENVIRONMENT_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;repository_directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;repository_directory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;item_type_in_scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ITEM_TYPES_IN_SCOPE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DEFAULT_ITEM_TYPES&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;token_credential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;exclude_paths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EXCLUDE_FOLDERS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twelve variables in total, covering authentication, the target workspace, the&lt;br&gt;
source directory, and switches for validation and cleanup.&lt;/p&gt;
&lt;h2&gt;
  
  
  Validate-only is not a no-op
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;develop&lt;/code&gt; branch runs the same script with &lt;code&gt;VALIDATE_ONLY=true&lt;/code&gt;. Ordering&lt;br&gt;
matters more than it looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;workspace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_workspace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;# authenticates, resolves, parses
&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;read_bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VALIDATE_ONLY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Validation-only mode. No workspace changes applied.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;read_bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REMOVE_ORPHANS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;unpublish_all_orphan_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;publish_all_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Constructing &lt;code&gt;FabricWorkspace&lt;/code&gt; is what authenticates the service principal,&lt;br&gt;
resolves the target workspace and parses every item in the repository. It is the&lt;br&gt;
validation. Return before it and every &lt;code&gt;develop&lt;/code&gt; build goes green having checked&lt;br&gt;
nothing.&lt;/p&gt;

&lt;p&gt;Worth a test that asserts validation actually validates, because a refactor that&lt;br&gt;
moves that line will not otherwise fail anything.&lt;/p&gt;
&lt;h2&gt;
  
  
  Branch gating
&lt;/h2&gt;

&lt;p&gt;Three stages, each gated only by the branch. No &lt;code&gt;dependsOn&lt;/code&gt; between them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy_UAT&lt;/span&gt;
  &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/release'))&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VG-Fabric-UAT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no automatic DEV to UAT to PROD chain. Promotion happens when someone&lt;br&gt;
merges a pull request, which is where review happens. A commit never walks itself&lt;br&gt;
into production.&lt;/p&gt;
&lt;h2&gt;
  
  
  Warehouse is excluded on purpose
&lt;/h2&gt;

&lt;p&gt;Sixteen item types are in scope. Warehouse is not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;DEFAULT_ITEM_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DataPipeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lakehouse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Notebook&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SemanticModel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# "Warehouse" is intentionally excluded. Warehouse schema deployment must
&lt;/span&gt;    &lt;span class="c1"&gt;# be handled separately to avoid schema reset risk during publish.
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Environment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Eventhouse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Eventstream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;KQLDatabase&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Publishing a warehouse through this path can reset its schema. An automation&lt;br&gt;
that handles most cases and silently corrupts the rest is worse than one that&lt;br&gt;
refuses the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually breaks
&lt;/h2&gt;

&lt;p&gt;The code above is not where deployments fail. They fail on prerequisites.&lt;/p&gt;

&lt;p&gt;I ended up writing them down and there are 43, across six categories: Fabric&lt;br&gt;
environment (6), identity and security (10), Azure DevOps configuration (10),&lt;br&gt;
deployment configuration (7), validation (4), post-deployment verification (6).&lt;/p&gt;

&lt;p&gt;Identity is the largest, and three of those ten are the same check repeated per&lt;br&gt;
environment: is the service principal actually a member of this workspace. It&lt;br&gt;
needs to be added to DEV, UAT and PROD separately. Nothing in the repository&lt;br&gt;
indicates when it is missing.&lt;/p&gt;

&lt;p&gt;That produces a misleading failure mode. A missing permission surfaces as an&lt;br&gt;
error thrown by the deployment step, which is the last thing in the chain and&lt;br&gt;
the first thing you look at. The error is real, the deployment did fail, and the&lt;br&gt;
code is fine.&lt;/p&gt;

&lt;p&gt;The checklist is in the repo at &lt;code&gt;docs/checklists/DEPLOYMENT_READINESS_CHECKLIST.md&lt;/code&gt;.&lt;br&gt;
Of everything this produced, it is the part I would copy first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment template
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TENANT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;TARGET_WORKSPACE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;TARGET_ENVIRONMENT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DEV
&lt;span class="nv"&gt;REPOSITORY_DIRECTORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./.
&lt;span class="nv"&gt;VALIDATE_ONLY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;REMOVE_ORPHANS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;ENABLE_SHORTCUT_PUBLISH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;INFO
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep &lt;code&gt;VALIDATE_ONLY=true&lt;/code&gt; until you are sure the target is right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No approval gates. A merge to &lt;code&gt;main&lt;/code&gt; deploys production unattended.&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;parameter.yml&lt;/code&gt;, so items referencing a workspace-specific resource by GUID
still point at the source environment after deployment.&lt;/li&gt;
&lt;li&gt;One solution per repository. The layout and variable naming assume it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stack: &lt;code&gt;fabric-cicd&lt;/code&gt; 1.2.0, Python 3.12, &lt;code&gt;ubuntu-latest&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;Full walkthrough: [&lt;a href="https://www.linkedin.com/pulse/building-enterprise-microsoft-fabric-cicd-practical-guide-mintu-ghosh-f3dbf/" rel="noopener noreferrer"&gt;https://www.linkedin.com/pulse/building-enterprise-microsoft-fabric-cicd-practical-guide-mintu-ghosh-f3dbf/&lt;/a&gt;] &lt;/p&gt;

&lt;p&gt;Code: [&lt;a href="https://github.com/vedaforge-team/fabric-cicd-reference/tree/v1.0.0" rel="noopener noreferrer"&gt;https://github.com/vedaforge-team/fabric-cicd-reference/tree/v1.0.0&lt;/a&gt;].&lt;/p&gt;

&lt;p&gt;Next post covers what happens when a second team needs the same repository, and&lt;br&gt;
why "just add path filters" does not work.&lt;/p&gt;

</description>
      <category>microsoftfabric</category>
      <category>azuredevops</category>
      <category>cicd</category>
      <category>python</category>
    </item>
  </channel>
</rss>
