Many of my active projects utilize Sonarcloud analysis – roughly 50 lines of YAML over 7 GitHub Actions steps. While recently updating a new project, I noticed that much of this workflow I was a direct copy and paste – a perfect candidate for reuse. There are a few options for reusable actions, but in this case, this is a good opportunity to utilize a composite action. Composite actions allow you to combine multiple steps into one.
The current Sonarcloud YAML I use is below – it’s fairly long and involved. Most of this is right from the Sonarcloud docs, so we aren’t going to spend any time trying to understand it, just migrate it to our next action.
sonarCloud:
name: Run SonarCloud analysis
runs-on: windows-latest
steps:
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 1.11
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Install .NET 7 SDK
uses: actions/setup-dotnet@v3.0.3
with:
dotnet-version: 7.0.x
- name: Cache SonarCloud packages
uses: actions/cache@v3
with:
path: ~\sonar\cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Cache SonarCloud scanner
id: cache-sonar-scanner
uses: actions/cache@v3
with:
path: .\.sonar\scanner
key: ${{ runner.os }}-sonar-scanner
restore-keys: ${{ runner.os }}-sonar-scanner
- name: Install SonarCloud scanner
if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
shell: powershell
run: |
New-Item -Path .\.sonar\scanner -ItemType Directory
dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
- name: Build and analyze
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
shell: powershell
run: |
.\.sonar\scanner\dotnet-sonarscanner begin /k:"samsmithnz_DotNetCensus" /o:"samsmithnz-github" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io"
dotnet build src/DotNetCensus/DotNetCensus.csproj
dotnet build src/DotNetCensus.Core/DotNetCensus.Core.csproj
dotnet build src/DotNetCensus.Tests/DotNetCensus.Tests.csproj
.\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}"
Creating the new action
The first step is to create a new public repo – as each reusable actions should be in their own repo. It is possible to create this in a private or internal repo with a PAT token – but we aren’t going to cover this today, creating our action in a public repo. A separate repo allows you to version and manage the composite action easily.
After creating the new repo, we need to create a new “action.yml” file at the root of the repo. This will contain the parameters to call our action and the logic. We will break it up and talk about this file in two parts.
First part we add is inputs. These are the variables, the parameters we will vary between each run. We need 5 inputs for our new script.
- A projects string, comma separated, with a path to each project we will scan
- A .NET version, to specify which version to setup on the hosted runner
- The Sonarcloud organization we are publishing to
- The Sonarcloud project we are publishing to
- The SONAR_TOKEN – secret API key to publish our project to Sonarcloud
That’s it – each parameter has a description to make it clear how to use it, and all of the parameters have the required flag set to true. Additionally, the dotnet-version parameter has a default value – as most of the time we will be using .NET 7. Note that SONAR_TOKEN will be a secret, but composite actions are smart enough to pass it in the secret and not expose it in our logs (if you echo it out, it will show “***”). The inputs are below:
name: 'Sonar Cloud composite action to analyze .NET projects'
description: 'Process .NET test results'
inputs:
projects:
description: 'comma separated list of projects to analyze. Reminder to use the / separator to be compatible with Linux'
required: true
dotnet-version:
description: 'the .NET version to use on .NET Setup'
required: true
default: '7.0.x'
sonarcloud-organization:
description: 'the sonarcloud organization name. For example, "samsmithnz-github"'
required: true
sonarcloud-project:
description: 'the sonarcloud project name. For example, "samsmithnz_dotnetsample"'
required: true
SONAR_TOKEN:
description: 'the sonar token secret. Note that GitHub will keep this as a secret (***) as long is the input is a secret'
required: true
Next: the logic, where we start by copying all of the steps from our original script. We also add a custom script to validate our inputs (there is a little bit of this built in internally to the composite action inputs too). Note that exit 1
command – that allows you to fail a step. I can enter (nearly) any number, and it will fail with that error code.
Finally, I replace variables with inputs as needed – for example the dotnet task, I use ${{ inputs.dotnet-version }}
. I also used a loop to split apart my comma separated string of paths into a separate line for each project. The logic is below:
runs:
using: 'composite'
steps:
- name: 'Pre-action checks'
shell: pwsh
run: |
#Validate inputs
if ("${{ inputs.projects }}" -eq $null)
{
echo "Please enter one of more projects, comma separated, input"
exit 1
}
if ("${{ inputs.sonarcloud-organization }}" -eq $null)
{
echo "Please enter a sonar organization input"
exit 1
}
if ("${{ inputs.sonarcloud-project }}" -eq $null)
{
echo "Please enter a sonarcloud project input"
exit 1
}
if ("${{ inputs.SONAR_TOKEN }}" -eq $null)
{
echo "Please enter a sonar token input"
exit 1
}
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 1.11
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Install .NET 7 SDK
uses: actions/setup-dotnet@v3.0.3
with:
dotnet-version: ${{ inputs.dotnet-version }}
- name: Cache SonarCloud packages
uses: actions/cache@v3
with:
path: ~/sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Cache SonarCloud scanner
id: cache-sonar-scanner
uses: actions/cache@v3
with:
path: ./.sonar/scanner
key: ${{ runner.os }}-sonar-scanner
restore-keys: ${{ runner.os }}-sonar-scanner
- name: Install SonarCloud scanner
if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
shell: pwsh
run: |
New-Item -Path ./.sonar/scanner -ItemType Directory
dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner
- name: Build and analyze
shell: pwsh
run: |
./.sonar/scanner/dotnet-sonarscanner begin /k:"${{ inputs.sonarcloud-project }}" /o:"${{ inputs.sonarcloud-organization }}" /d:sonar.login="${{ inputs.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io"
$projectArray = "${{ inputs.projects}}".Split(",")
for ($i = 0; $i -lt $projectArray.Length; $i++)
{
dotnet build $($projectArray[$i])
}
./.sonar/scanner/dotnet-sonarscanner end /d:sonar.login="${{ inputs.SONAR_TOKEN }}"
Now we have our action, we create an action that will version, test, and then release this action to this repo. We’ve covered versioning and releasing in other posts, so won’t cover it here, but the important part about testing our action is that you can run your own action with the uses: ./
command. If we combine this with the ability to check out the feature branch, (using the ref
property of uses: actions/checkout@v3
task), we can now validate changes on the feature branch, instead of waiting to see what happens after the action is released. Below is the Linux version of the test – we do run both a Linux and Windows version to ensure it works for both operating systems – I prefer to build with Linux as it’s cheaper and faster – but I do have some Windows apps that require a Windows runner. Note that I could have used a matrix here, but elected not to, for simplicity.
linuxTest:
runs-on: ubuntu-latest
steps:
- name: checkout the code from this branch
uses: actions/checkout@v3
with:
ref: ${{ github.ref }}
- name: Test this repo
uses: ./ # the ./ runs the action.yml in this repo
with:
projects: 'Sample\ConsoleApp1\ConsoleApp1.csproj'
dotnet-version: '7.0.x'
sonarcloud-organization: 'samsmithnz-github'
sonarcloud-project: 'samsmithnz_SamsDotNetSonarCloudAction'
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
Now our action is building, is tested, and has been released, we can consume it in a real project, referencing the new action with its path and current version: uses: samsmithnz/SamsDotNetSonarCloudAction@1.0.5
. Going back to our original YAML, we can replace what we had above, with this – down to 12 lines! The Pull Request is available here to see the complete change.
sonarCloud:
name: Run SonarCloud analysis
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Run Sonarcloud test
uses: samsmithnz/SamsDotNetSonarCloudAction@1.0.5
with:
projects: 'src/DotNetCensus/DotNetCensus.csproj,src/DotNetCensus.Core/DotNetCensus.Core.csproj,src/DotNetCensus.Tests/DotNetCensus.Tests.csproj'
dotnet-version: '7.0.x'
sonarcloud-organization: 'samsmithnz-github'
sonarcloud-project: 'samsmithnz_DotNetCensus'
SONAR_TOKEN: '${{ secrets.SONAR_TOKEN }}'
Wrap-up
Today we created our own composite action, incorporating versioning, testing, releases, and improved our code – without significantly adding to the project complexity. As an added bonus- any updates or fixes to the new composite action will be picked up by Dependabot and updated too. It’s easy to create your own composite action, and I encourage you do it where it makes sense.