6 min read

Building Custom GitHub Action with .NET Core

Justin Yoo

Previously, I wrote several blog posts about GitHub Actions, and I'm going to continue it in this post. We've been using ready-made GitHub Actions so far. However, there might not be Actions available, which I'm looking for, or I can't use the open-sourced Actions for various reasons. In this instance, we need to build our own Custom GitHub Action. Throughout this post, I'm going to discuss when to use Custom GitHub Actions and how to build it, with a very simple .NET Core console application.

Why Custom GitHub Actions?

All GitHub Actions available on the marketplace are open-sourced. Anyone who agrees with the owner's license policy can use them. At the same time, someone can't use them because of the license policy. Maybe their organisation doesn't allow to use open-source libraries unless it's fully endorsed. In this case, instead of using the existing Actions, they should build their own Actions. Let's think about other cases. What if there's no Action that I'm looking for? I should wait for the Action published by someone else or build it by myself. If I decide to make it, the Custom GitHub Action is the answer.

Types of Custom GitHub Actions

There are two types of building Custom Actions. One is to use Docker container, and the other is to use JavaScript. Both have their own pros and cons. Here are some:

Docker Action JavaScript Action
Runner Dependency Runner independent Runner Dependent
Performance Slow Fast
Multi-platform Support Ubuntu runner only All runners
Language Support All languages available JavaScript only

The reason why the Docker Action is slower than the JavaScript one is that it takes time to build a container before use, while the JavaScript one runs directly on the runner. With this regards, the JavaScript one looks way better, but multi-language support is the killing point of using the Docker Action. If you want to use your preferred language, like C#, Java, Python, Go, or PHP, the Docker Action is yours.

As we're building a .NET Core console application written in C#, we're going to use the Docker one.

Building a .NET Core Console Application

First of all, let's write a .NET Core console app. Instead of a simple Hello World style one, we're building a more practical one. The code below takes inputs and send a message to a Microsoft Teams channel. Here's the code:

This post shows more detailed logic, used for Azure Functions.

public static class Program
public static void Main(string[] args)
var card = new MessageCard()
Title = args[1],
Summary = args[2],
Text = args[3],
ThemeColor = args[4],
Sections = ParseCollection<Section>(args[5]),
Actions = ParseCollection<BaseAction>(args[6])
var converted = JsonConvert.SerializeObject(card);
var message = (string)null;
var requestUri = args[0];
using (var client = new HttpClient())
using (var content = new StringContent(converted, Encoding.UTF8, "application/json"))
using (var response = await client.PostAsync(requestUri, content).ConfigureAwait(false))
private static List<T> ParseCollection<T>(string value)
var parsed = string.IsNullOrWhiteSpace(value)
? null
: JsonConvert.DeserializeObject<List<T>>(value, settings);
return parsed;
view raw program.cs hosted with ❤ by GitHub

Once completing the console app, let's move onto the custom action part.

action.yml – Custom Action Metadata

action.yml declares how the custom action work by defining input and output values and how it runs. At the root directory of the repository, create the action.yml file and fill the code like below. For more details of the metadata, please visit the Metadata syntax for GitHub Actions page.

name: <Name of Custom GitHub Action>
description: <Short description of Action>
description: <Short description of input parameter>
required: <true|false>
default: <default value>
using: docker
image: Dockerfile
INPUT_PARAMETER: {{ inputs.input_parameter }}
view raw action.yml hosted with ❤ by GitHub

Both using and image parameters are the core of this declaration. using MUST be docker and image SHOULD be Dockerfile. It can be a reference from outside of the repository, but it's better to stay within the repo.

If you use the extension of .yaml, like action.yaml, it can't read the secrets from the repository settings, which is a BUG at the time of this writing. Therefore, keep using the .yml extension for now. Of course, the main workflow can use both .yaml and .yml.

Dockerfile – Container Definition

In the previous section, the action.yml refers to Dockerfile. As we're building a .NET Core application, we need a base image containing .NET Core 3.1 SDK, which is mcr.microsoft.com/dotnet/core/sdk:3.1. Then copy the source code into the container image. Finally copy the entrypoint.sh file that executes the Docker container image.

FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
COPY *.sln .
COPY src/ ./src/
ADD entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
view raw dockerfile.txt hosted with ❤ by GitHub

entrypoint.sh – Container Runner

We copied the entrypoint.sh file into Dockerfile. How does it look like? In the Dockerfile, there is nothing but copying the source code. Therefore, the entrypoint.sh should build the app and run it with the arguments passed. Here's the code:

#!/bin/sh -l
cd /app
dotnet restore
dotnet build
dotnet run --project src/GitHubActions.Teams.ConsoleApp -- \
--input-parameter "$INPUT_PARAMETER"
view raw entrypoint.sh hosted with ❤ by GitHub

Now, we've got the basic structure of a Custom Action. Let's have a test!

Build Private Workflow

If I want to know whether my Action works OK or not, I should create a workflow and run it. Create a workflow, .github/workflows/main.yaml like:

name: Build and Test GitHub actions
on: push
name: Build and test the GitHub Action
runs-on: ubuntu-latest
- name: Checkout the repository
uses: actions/checkout@v1
- name: Run the private action
uses: ./
input_parameter: <Input parameter value>
view raw workflow.yaml hosted with ❤ by GitHub

Once running the workflow, I'll have a message on my Microsoft Teams channel.

README.md – Usage Instruction

If you want to open your custom action, everyone should be able to know how to use it. Write a README.md file so that potential users can understand how to use your Action. It should at least contain the followings:

  • A detailed description of what the Action does.
  • Required input and output arguments.
  • Optional input and output arguments.
  • Secrets the Action uses.
  • Environment variables the Action uses.
  • An example of how to use your Action in a workflow.

Once you publish yours to marketplace, you'll be able to see like this:

And here's the actual link of the Custom Action:

So far, we've walked through how to build a custom action with a real-world scenario. If you have your use case, why not creating a one? It's your turn now.