Automate code metrics and class diagrams with GitHub Actions

David Pine

Hi friends, in my previous post — .NET 💜 GitHub Actions, you were introduced to the GitHub Actions platform and the workflow composition syntax. You learned how common .NET CLI commands and actions can be used as building blocks for creating fully automated CI/CD pipelines, directly from your GitHub repositories.

This is the second post in the series dedicated to the GitHub Actions platform and the relevance it has for .NET developers. In this post, I’ll summarize an existing GitHub Action written in .NET that can be used to maintain a code metrics markdown file. I’ll then explain how you can create your own GitHub Actions with .NET. I’ll show you how to define metadata that’s used to identify a GitHub repo as an action. Finally, I’ll bring it all together with a really cool example: updating an action in the .NET samples repository to include class diagram support using GitHub’s brand new Mermaid diagram support, so it will build updated class diagrams on every commit.

Writing a custom GitHub Action with .NET

Actions support several variations of app development:

Action type Metadata specifier Description
Docker runs.using: 'docker' Any app that can run as a Docker container.
JavaScript runs.using: 'javascript' Any Node.js app (includes the benefit of using actions/toolkit).
Composite runs.using: 'composite' Composes multiple run commands and uses actions.

.NET is capable of running in Docker containers, and when you want a fully functioning .NET app to run as your GitHub Action — you’ll have to containerize your app. For more information on .NET and Docker, see .NET Docs: Containerize a .NET app.

If you’re curious about creating JavaScript GitHub Actions, I wrote about that too, see Localize .NET applications with machine-translation. In that blog post, I cover a TypeScript action that relies on Azure Cognitive Services to automatically generate pull requests for target translations.

In addition to containerizing a .NET app, you could alternatively create a .NET global tool that could be installed and called upon using the run syntax instead of uses. This alternative approach is useful for creating a .NET CLI app that can be used as a global tool, but it’s out of scope for this post. For more information on .NET global tools, see .NET Docs: Global tools.

Intent of the tutorial

Over on the .NET Docs, there’s a tutorial on creating a GitHub Action with .NET. It covers exactly how to containerize a .NET app, how to author the action.yml which represents the action’s metadata, as well as how to consume it from a workflow. Rather than repeating the entire tutorial, I’ll summarize the intent of the action, and then we’ll look at how it was updated.

The app in the tutorial performs code metric analysis by:

  • Scanning and discovering *.csproj and *.vbproj project files in a target repository.
  • Analyzing the discovered source code within these projects for:
    • Cyclomatic complexity
    • Maintainability index
    • Depth of inheritance
    • Class coupling
    • Number of lines of source code
    • Approximated lines of executable code
  • Creating (or updating) a file.

As part of the consuming workflow composition, a pull request is conditionally (and automatically) created when the file changes. In other words, as you push changes to your GitHub repository, the workflow runs and uses the .NET code metrics action — which updates the markdown representation of the code metrics. The file itself is navigable with automatic links, and collapsible sections. It uses emoji to highlight code metrics at a glance, for example, when a class has high cyclomatic complexity it bubbles an emoji up to the project level heading the markdown. From there, you can drill down into the class and see the metrics for each method.

The Microsoft.CodeAnalysis.CodeMetrics namespace contains the CodeAnalysisMetricData type, which exposes the CyclomaticComplexity property. This property is a measurement of the structural complexity of the code. It is created by calculating the number of different code paths in the flow of the program. A program that has a complex control flow requires more tests to achieve good code coverage and is less maintainable. When the code is analyzed and the GitHub Action updates the file, it writes an emoji in the header using the following code:

internal static string ToCyclomaticComplexityEmoji(
    this CodeAnalysisMetricData metric) =>
    metric.CyclomaticComplexity switch
        >= 0 and <= 7 => ":heavy_check_mark:",  // ✔️
        8 or 9 => ":warning:",                  // ⚠️
        10 or 11 => ":radioactive:",            // ☢️
        >= 12 and <= 14 => ":x:",               // ❌
        _ => ":exploding_head:"                 // 🤯

All of the code for this action is provided in the .NET samples repository and is also part of the .NET docs code samples browser experience. As an example of usage, the action is self-consuming (or dogfooding). As the code updates the action runs, and maintains a file via automated pull requests. Consider the following screen captures showing some of the major parts of the markdown file: heading

.NET code metrics markdown sample top-level heading. ProjectFileReference drill-down heading

.NET code metrics markdown sample drill-down.

The code metrics markdown file represents the code it analyzes, by providing the following hierarchy:

Project ➡️ Namespace ➡️ Named Type ➡️ Members table

When you drill down into a named type, there is a link at the bottom of the table for the auto-generated class diagram. This is discussed in the Adding new functionality section. To navigate the example file yourself, see .NET samples

Action metadata

For a GitHub repository to be recognized as a GitHub Action, it must define metadata in an action.yml file.

# Name, description, and branding. All of which are used for
# displaying the action in the GitHub Action marketplace.
name: '.NET code metric analyzer'
description: 'A GitHub action that maintains a file,
              reporting cyclomatic complexity, maintainability index, etc.'
  icon: sliders
  color: purple

# Specify inputs, some are required and some are not.
    description: 'The owner of the repo. Assign from github.repository_owner. Example, "dotnet".'
    required: true
    description: 'The repository name. Example, "samples".'
    required: true
    description: 'The branch name. Assign from github.ref. Example, "refs/heads/main".'
    required: true
    description: 'The root directory to work from. Example, "path/to/code".'
    required: true
    description: 'The workspace directory.'
    required: false
    default: '/github/workspace'

# The action outputs the following values.
    description: 'The title of the code metrics action.'
    description: 'A detailed summary of all the projects that were flagged.'
    description: 'A boolean value, indicating whether or not the 
                  was updated as a result of running this action.'

# The action runs using docker and accepts the following arguments.
  using: 'docker'
  image: 'Dockerfile'
  - '-o'
  - ${{ inputs.owner }}
  - '-n'
  - ${{ }}
  - '-b'
  - ${{ inputs.branch }}
  - '-d'
  - ${{ inputs.dir }}
  - '-w'
  - ${{ inputs.workspace }}

The metadata for the .NET samples code metrics action is nested within a subdirectory, as such, it is not recognized as an action that can be displayed in the GitHub Action marketplace. However, it can still be used as an action.

For more information on metadata, see GitHub Docs: Metadata syntax for GitHub Actions.

Consuming workflow

To consume the .NET code metrics action, a workflow file must exist in the .github/workflows directory from the root of the GitHub repository. Consider the following workflow file:

name: '.NET code metrics'

    branches: [ main ]
    # Ignore and files
    - '**.md'

    runs-on: ubuntu-latest
        contents: write
        pull-requests: write

    - uses: actions/checkout@v2

    # Analyze repositories source metrics:
    # Create (or update) file.
    - name: .NET code metrics
      id: dotnet-code-metrics
      uses: dotnet/samples/github-actions/DotNet.GitHubAction@main
        owner: ${{ github.repository_owner }}
        name: ${{ github.repository }}
        branch: ${{ github.ref }}
        dir: ${{ './github-actions/DotNet.GitHubAction' }}

    # Create a pull request if there are changes.
    - name: Create pull request
      uses: peter-evans/create-pull-request@v3.4.1
      if: ${{ steps.dotnet-code-metrics.outputs.updated-metrics }} == 'true'
        title: '${{ steps.dotnet-code-metrics.outputs.summary-title }}'
        body: '${{ steps.dotnet-code-metrics.outputs.summary-details }}'
        commit-message: '.NET code metrics, automated pull request.'

This workflow makes use of jobs.<job_id>.permissions, setting contents and pull-requests to write. This is required for the action to update contents in the repo and create a pull request from those changes. For more information on permissions, see GitHub Docs: Workflow syntax for GitHub Actions – permissions.

To help visualize how this workflow functions, see the following sequence diagram:

GitHub .NET code metrics workflow sequence diagram.

The preceding sequence diagram shows the workflow for the .NET code metrics action:

  1. When a developer pushes code to the GitHub repository.
    1. The workflow is triggered and starts to run.
  2. The source code is checked out into the $GITHUB_WORKSPACE.
  3. The .NET code metrics action is invoked.
    1. The source code is analyzed, and the file is updated.
  4. If the .NET code metrics step (dotnet-code-metrics) outputs that metrics were updated, the create-pull-request action is invoked.
    1. The file is checked into the repository.
    2. An automated pull request is created. For example pull requests created by the app/github-actions bot, see .NET samples / pull requests.

Adding new functionality

GitHub recently announced diagram support for Markdown powered by Mermaid. Since our custom action is capable of analyzing C# as part of its execution, it has a semantic understanding of the classes it’s analyzing. This is used to automatically create Mermaid class diagrams in the file.

The .NET code metrics GitHub Action sample code was updated to include Mermaid support.

static void AppendMermaidClassDiagrams(
    MarkdownDocument document,
    List<(string Id, string Class, string MermaidCode)> diagrams)
    document.AppendHeader("Mermaid class diagrams", 2);

    foreach (var (id, className, code) in diagrams)
        document.AppendParagraph($"<div id="{id}"></div>");
        document.AppendHeader($"`{className}` class diagram", 5);
        document.AppendCode("mermaid", code);

If you’re interested in seeing the code that generates the diagrams argument, see the ToMermaidClassDiagram extension method. As an example of what this renders like within the CODE_METRICS.mdMarkdown file, see the following diagram:

.NET code metrics markdown class diagram.


In this post, you learned about the different types of GitHub Actions with an emphasis on Docker and .NET. I explained how the .NET code metrics GitHub Action was updated to include Mermaid class diagram support. You also saw an example action.yml file that serves as the metadata for a GitHub Action. You then saw a visualization of the consuming workflow, and how the code metrics action is consumed. I also covered how to add new functionality to the code metrics action.

What will you build, and how will it help others? I encourage you to create and share your own .NET GitHub Actions. For more information on .NET and custom GitHub Actions, see the following resources:


Comments are closed. Login to edit/delete your existing comments

  • Mark Adamson

    Great examples, thanks! Auto code diagrams using mermaid is a super idea

  • mu88

    Thanks for the interesting examples! If you need inspiration for a new blog post 😉 I’m looking for a way to display a code coverage badge on my, as it was possible with Azure DevOps. With GitHub Actions, you can create those badges, but you have to store them somewhere explicitly (e. g. in a cloud or as an image within the repo). But commiting build artifacts (like coverage reports) to the repo does not seem appropriate to me.

  • Nicholas Tyrrell

    This is extremely cool to see! Would it be possible to see a suggested approach for how to do something similar in Azure DevOps? I’ve tried to accomplish this myself, but it seems to be a lot less straightforward.