Why You Should Add a Dedicated Linting Stage for Bicep Templates

When working with Bicep for Infrastructure-as-Code (IaC), ensuring the quality of your templates isn’t just a good idea—it’s critical. A well-maintained pipeline ensures that your deployments are efficient, reliable, and meet organizational standards. While the Bicep CLI’s built-in linter can catch many issues, is relying solely on the what-if deployment stage really enough? Or should we go the extra mile and introduce a dedicated linting stage in our CI/CD pipeline?

In this post, I’ll break down the pros and cons of a standalone linting stage, highlight scenarios where it’s a must-have, and show you how to implement one.


The Role of the Bicep Linter

The Bicep linter is your first line of defense against:

  • Hardcoded environment values.
  • Missing or incorrect template properties.
  • Naming convention violations.
  • Deviations from best practices.

It’s easy to configure via a bicepconfig.json file, giving you the flexibility to adjust rules or suppress specific warnings based on your team’s needs. However, while it integrates seamlessly with commands like bicep build and what-if, relying on these alone can have drawbacks.


Why Add a Separate Linting Stage?

A separate linting stage in your CI/CD pipeline isn’t just about catching errors—it’s about catching them at the right time. Here’s what it brings to the table:

1. Faster Feedback Loops

Linting is lightweight compared to the more complex what-if operation. Running it as a standalone stage allows developers to get immediate feedback, addressing issues before they become blockers in later stages.

2. Streamlined Debugging

Let’s face it: scrolling through deployment logs to pinpoint linting errors can be frustrating. A dedicated linting stage isolates these issues, making them easier to spot and resolve.

3. Enforcing Quality Gates

Think of a linting stage as your team’s "checkpoint." It ensures that every template adheres to coding standards and best practices before moving forward.

4. Separation of Concerns

Keeping linting separate from resource evaluation aligns with clean pipeline design principles. This separation makes troubleshooting easier and keeps your pipeline logic manageable.


When Is a Separate Linting Stage Essential?

Not every team will need a dedicated linting stage, but in the following scenarios, it’s indispensable:

1. Large Teams with Governance Policies

In larger organizations, compliance isn’t optional. A linting stage ensures that templates meet strict standards, like naming conventions and security policies, before deployment begins.

2. Early Detection of Errors

Imagine your policy prohibits hardcoded URLs for database connections (e.g., database.windows.net). Catching this in the what-if stage could slow your feedback loop. A linting stage flags it earlier, saving valuable time.

3. Modular Pipelines

If your project uses modular Bicep templates (e.g., parent and child files), a linting stage ensures that issues in one module don’t cascade into others.


Pipeline Example: Adding a Linting Stage

Here’s an example Azure DevOps pipeline that integrates a dedicated linting stage before the what-if stage:

  
  trigger:
  branches:
    include:
      - main
      - feature/*

stages:
  - stage: Linting
    displayName: "Lint Bicep Templates"
    jobs:
      - job: Lint
        displayName: "Run Bicep Linter"
        steps:
          - task: UseBicepCLI@0
            displayName: "Install Bicep CLI"
            inputs:
              version: 'latest'
          - script: |
              bicep lint --file ./main.bicep
            displayName: "Run Linter"
  
  - stage: WhatIf
    dependsOn: Linting
    displayName: "Azure What-If Deployment"
    jobs:
      - deployment: WhatIf
        environment: 'Azure'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureCLI@2
                  displayName: "Run What-If"
                  inputs:
                    azureSubscription: '<YourSubscription>'
                    scriptType: bash
                    scriptLocation: inlineScript
                    inlineScript: |
                      az deployment group what-if \
                        --resource-group '<YourResourceGroup>' \
                        --template-file ./main.bicep \
                        --parameters @parameters.json

Real-World Example: Catching Hardcoded URLs

Let’s say your organization prohibits hardcoded URLs in Bicep files. If you rely on the what-if stage to catch these, you’re potentially delaying remediation. By adding a linting stage, the linter flags these violations upfront, giving developers a chance to resolve them quickly.

Here’s how you can enforce this with a simple configuration:

{
  "analyzers": {
    "core": {
      "rules": {
        "no-hardcoded-environment-urls": {
          "level": "error"
        }
      }
    }
  }
}

Addressing Common Questions

Isn’t Real-Time Linting in VS Code Enough?
Not always. While VS Code’s linter is great for local development, it can’t enforce standards consistently across your team. A centralized linting stage ensures that every commit is validated against the same rules.

Won’t This Add Complexity?
It’s a trade-off. Adding a linting stage does introduce more pipeline steps, but the benefits—faster feedback, improved debugging, and consistent enforcement—far outweigh the cost, especially for production-grade workflows.


Conclusion

A dedicated linting stage might seem like overkill for small projects, but for teams with strict governance policies, modular pipelines, or a need for fast feedback loops, it’s a game-changer. It simplifies debugging, enforces quality gates, and ensures that every template meets your organization’s standards.

What’s your take? Have you faced challenges with Bicep linting in your pipelines? Share your experiences in the comments—I’d love to hear your thoughts!