Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add bicep linting #817

Merged
merged 14 commits into from Jan 30, 2024
19 changes: 19 additions & 0 deletions .github/workflows/build-and-deploy-static-web-app.yml
Expand Up @@ -55,6 +55,25 @@ jobs:

echo "DEPLOYMENT_NAME=$DEPLOYMENT_NAME" >> $GITHUB_OUTPUT

- name: Lint Bicep
if: github.event_name == 'pull_request'
uses: azure/CLI@v1
with:
inlineScript: |
az version
az bicep install
az bicep version
az bicep lint --file ./infra/main.bicep --diagnostics-format sarif > bicep.sarif
echo '::warning::Bicep linting results:'
cat bicep.sarif

- name: Upload SARIF
if: github.event_name == 'pull_request' && (success() || failure())
uses: github/codeql-action/upload-sarif@v3
with:
category: bicep
sarif_file: bicep.sarif

- name: Infra - detect changes & validate 📝
id: static_web_app_what_if
if: github.event_name == 'pull_request'
Expand Down
@@ -0,0 +1,221 @@
---
slug: bicep-lint-azure-pipelines-github-actions
title: 'Bicep lint with Azure Pipelines and GitHub Actions'
authors: johnnyreilly
tags: [bicep, github actions, azure pipelines, azure devops]
image: ./title-image.png
description: Bicep lint allows you to lint bicep files to ensure they conform to best practices. In this post we'll look at how to run bicep lint in Azure Pipelines and GitHub Actions.
hide_table_of_contents: false
---

Bicep has had linting [since version 0.4.1](https://github.com/Azure/bicep/releases/tag/v0.4.1). It's a great way to ensure that your bicep files conform to best practices. Interestingly, when the linting feature first shipped, there wasn't an explicit lint command as part of the CLI. Instead, you had to run `bicep build` and it would run the linter as part of the build process. This was a little confusing as it was not obvious that the linter was running.

As of [version 0.21.1](https://github.com/Azure/bicep/releases/tag/v0.21.1) there is a dedicated [`bicep lint`](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/linter) command. This is a nice step forwards; it allows you to explicitly lint your your code, rather than have it happen as a side effect of build. And it is useful if you want to run the linter as part of a CI/CD pipeline. What's more the `bicep lint` command is now available in the Azure CLI as well. You can run [`az bicep lint`](https://docs.microsoft.com/en-us/cli/azure/bicep?view=azure-cli-latest#az-bicep-lint) to lint your bicep files.

In this post we'll look at how to run lint Bicep in Azure Pipelines and GitHub Actions, and surface the output in the UI.

![title image reading "Bicep lint with Azure Pipelines and GitHub Actions" with the Bicep logo](title-image.png)

<!--truncate-->

## The general approach

The general approach is the same for both Azure Pipelines and GitHub Actions. One way or another, we'll run the Bicep `lint` command to lint our Bicep files and capture the output. As yet, there is no option to export the results of the lint command as a file. This may come, and [there is a discussion about it](https://github.com/Azure/bicep/issues/11960). However, there is a way to achieve our goal, which came out in discussion with [Anthony Martin](https://github.com/anthony-c-martin) of the Bicep team. We can write the output of the `lint` command to a file like so:

```
bicep lint main.bicep --diagnostics-format sarif > lint.sarif
```

This will write the output of the `lint` command to a file called `lint.sarif`. This is a [SARIF](https://sarifweb.azurewebsites.net/) file. SARIF stands for Static Analysis Results Interchange Format. It's a standard for representing the results of static analysis tools. It's a JSON file, easy to parse and has integrations with GitHub Actions / Azure Pipelines.

In the example above we directly used the `bicep lint` command. An alternative approach is to use the Azure CLI like so:

```
az bicep lint --file main.bicep --diagnostics-format sarif > lint.sarif
```

This is a little more verbose, but it does mean that you don't need to install the Bicep CLI on your build agent. You can use the Azure CLI instead; and this is already a first class citizen of Azure Pipelines and GitHub Actions, with dedicated support. (That said, there is a slight issue with this approach at the time of writing, which we'll come to later.)

However we generate it, we can take the SARIF file and use it to surface the results of the linting process in Azure Pipelines and GitHub Actions.

## Linting Bicep in GitHub Actions with the Azure CLI

We'll start off by looking at how to lint Bicep in GitHub Actions with the Azure CLI. But before we do that, we need something to lint. Within your `main.bicep` file you should have something like this:

```bicep
var unusedVar = 1 // unused variable
```

As it suggests, this is an unused variable. We're just using this to demonstrate the linting process. And alongside that, we want a [`bicepconfig.json`](https://aka.ms/bicep/config) file that looks like this:

```json
{
"analyzers": {
"core": {
"rules": {
"no-unused-vars": {
"level": "warning"
}
}
}
}
}
```

This explicitly enables the [`no-unused-vars`](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/linter-rule-no-unused-variables) rule, and sets it to `warning` level. This means that the linter will warn us about unused variables, but it won't fail the build. We could set it to `error` level, and then the build would fail if there were any unused variables. We'll come back to this later.

In a GitHub workflow in your repository you should have steps like these:

```yaml
- name: Lint Bicep
uses: azure/CLI@v1
with:
inlineScript: |
az bicep install
az bicep lint --file ./infra/main.bicep --diagnostics-format sarif > bicep.sarif

- name: Upload SARIF
if: (success() || failure())
uses: github/codeql-action/upload-sarif@v3
with:
category: bicep
sarif_file: bicep.sarif
```

The above:

- Installs Bicep to the Azure CLI
- Runs the `lint` command and writes the results to a file called `bicep.sarif`
- Uploads the SARIF file to GitHub

The `upload-sarif` action is provided by GitHub. [It allows surfacing the results of static analysis tools in GitHub](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/uploading-a-sarif-file-to-github). Doing this will show the results of the linting process in the GitHub UI, and it will also show them in the GitHub Security / Code Scanning UI, like so:

![screenshot of the no-unused-vars rule in GitHub UI](screenshot-github-actions-no-unused-vars.webp)

## Linting Bicep in GitHub Actions with the Bicep CLI

We can also lint Bicep in GitHub Actions with the Bicep CLI. At the time of writing, there's a reason you might want to favour this approach over the Azure CLI approach. I won't drill into it in depth, but at present if `az bicep lint` fails / returns a non-0 exit code then no output is produced. We could make this happen by updating the `bicepconfig.json` to dial up the `no-unused-vars` rule to `error` level, like so:

```json
{
"analyzers": {
"core": {
"rules": {
"no-unused-vars": {
"level": "error"
}
}
}
}
}
```

Now an unused variable will cause the build to fail. But the output of the `lint` command will not be surfaced in the GitHub UI. This is because the Azure CLI is not surfacing the output of the `lint` command when it fails.

The issue with the Azure CLI will hopefully be remedied; [you can track that here](https://github.com/Azure/azure-cli/issues/28259). For now you can use the Bicep CLI directly, where this isn't an issue. We'll do that now.

I'm basing this approach on Anthony Martin's example of [Bicep linting with GitHub Actions](https://github.com/anthony-c-martin/bicep-on-k8s/blob/e6dfa61fe7eae6fd6b148670f940041f3e294b20/.github/workflows/ci.yml#L36-L50):

```yml
- name: Setup Bicep
uses: anthony-c-martin/setup-bicep@v0.3

- name: Lint Bicep
run: |
bicep lint ./infra/main.bicep --diagnostics-format sarif > bicep.sarif

- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
category: bicep
sarif_file: bicep.sarif
```

The above does the same as the Azure CLI approach, but it uses the Bicep CLI directly. The [`setup-bicep` action](https://github.com/marketplace/actions/setup-bicep) is provided by Anthony Martin and installs the Bicep CLI on your build agent.

As I say, right now you may want to favour this approach over the Azure CLI approach to cover both surfacing warnings and errors. But hopefully that will change soon.

## Linting Bicep in Azure Pipelines with the Azure CLI

We've now seen how to lint Bicep in GitHub Actions with both the Azure CLI and the Bicep CLI. We can do the same in Azure Pipelines; but only the Azure CLI approach (as I'm not sure if there's a `setup-bicep` equivalent for Azure Pipelines).

How you want to surface the results in Azure Pipelines is up to you. You could surface into the "Scans" portion of Azure Pipelines UI or into "Tests". I'll show you how to do both.

### Surface the results in Scans

To surface the results in the scans part of Azure Pipelines you need to publish the SARIF file as a build artifact. You can do that like so:

```yml
jobs:
- job: LintInfra
displayName: Lint Infra
dependsOn: []
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureCLI@2
displayName: Lint main.bicep
inputs:
azureSubscription: service-connection-with-access-to-registry # you may not need this
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az bicep install
az bicep lint --file infra/main.bicep --diagnostics-format sarif > $(System.DefaultWorkingDirectory)/bicep.sarif

- task: PublishBuildArtifacts@1
condition: always()
inputs:
pathToPublish: $(System.DefaultWorkingDirectory)/bicep.sarif
artifactName: CodeAnalysisLogs # required to show up in the scans tab
```

The above is essentially the same as the GitHub Actions example, but it uses the Azure CLI instead of the Bicep CLI. The `PublishBuildArtifacts` task is provided by Azure Pipelines. It allows you to publish build artifacts, which will show up in the Scans part of Azure Pipelines. You can see the results of the linting process in Scans, like so:

![screenshot of the no-unused-vars rule in Azure Pipelines scans](screenshot-azure-pipelines-scans-no-unused-vars.webp)

### Surface the results in Tests

You can also surface the results in the tests part of Azure Pipelines. To do that we're going to borrow a [suggestion from Anthony Martin](https://github.com/Azure/bicep/issues/11960#issuecomment-1737226501) and use the [`sarif-junit`](https://www.npmjs.com/package/sarif-junit) package. This package allows us to convert a SARIF file to a JUnit file. JUnit is a standard for representing test results and can be used with the `PublishTestResults` task.

```yml
jobs:
- job: LintInfra
displayName: Lint Infra
dependsOn: []
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
displayName: 'Install Node.js'
inputs:
versionSpec: '18.x'

- task: AzureCLI@2
displayName: Lint main.bicep
inputs:
azureSubscription: service-connection-with-access-to-registry # you may not need this
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az bicep install
az bicep lint --file infra/main.bicep --diagnostics-format sarif > $(System.DefaultWorkingDirectory)/bicep.sarif
npx -y sarif-junit -i $(System.DefaultWorkingDirectory)/bicep.sarif -o $(System.DefaultWorkingDirectory)/bicep.xml

- task: PublishTestResults@2
condition: always()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '$(System.DefaultWorkingDirectory)/bicep.xml'
```

So the above is the same approach again but requires Node.js to be installed, and the results end up in the Tests part of Azure Pipelines. You can see the results of the linting process in Tests, like so:

![screenshot of the no-unused-vars rule in Azure Pipelines tests](screenshot-azure-pipelines-tests-no-unused-vars.webp)

## Summary

That's it! We've seen how to lint Bicep in Azure Pipelines and GitHub Actions. We've seen how to surface the results in the scans and tests parts of Azure Pipelines and in GitHub. We've seen how to do it with the Azure CLI and the Bicep CLI. And we've seen how to do it with warnings and errors. Hopefully this will help you to ensure that your Bicep files conform to best practices.

Many thanks to Anthony Martin for his help, which laid the groundwork for much of what we explored in this post.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions infra/bicepconfig.json
Expand Up @@ -7,6 +7,9 @@
"no-unused-params": {
"level": "warning"
},
"no-unused-vars": {
"level": "error"
},
"secure-secrets-in-params": {
"level": "error"
},
Expand Down