Creating a GitHub Composite Action

Posted by

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.

References

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s