Create a Custom GitHub Action in Rust
Tags: rust, githubactions, docker, cicd
Published: 2024-04-29 04:29:54

Anyone who gets into serious software development quickly realizes that the path to productivity leads through automation. This applies to both individuals as well as entire teams. When you automate repetitive development tasks, not only do you save time for your future self, but you also reduce human error, foster repeatability, and eliminate the bus factor.

Of course, there's a fine balance between automating too little and too much -- you probably don't want to spend way more time automating your tasks than it would actually take to perform them manually! This is where your tools and platforms come into play. While some tasks are "standard practices" that apply to every project of a certain type, and may be set up right at the start, high-value projects tend to evolve in unique ways that eventually require a great deal of customization.

## What are GitHub Actions?

If you've used GitHub while working on a software project, chances are you've seen [GitHub Actions](https://docs.github.com/en/actions) being utilized for automating tasks such as enforcing code formatting, running unit tests, checking code quality, or even packaging and publishing the final software product.

As an integral part of the GitHub platform, GitHub Actions are tightly integrated into your software development life-cycle, right where your code lives, and where much of your daily product development and collaboration happens. They are quite intuitive and easy to grasp; they use [domain language](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions#the-components-of-github-actions) that you're likely familiar with, such as workflows, jobs, and steps. Your workflows are described in YAML files that are stored in Git along with the rest of your code. Best of all, they are free to use, and GitHub even provides [free compute infrastructure](https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration) to run your workflows!

In addition to a set of commonly required actions provided directly by the platform itself, GitHub supports a rich [marketplace](https://github.com/marketplace?type=actions) of third-party actions that seem to cover any engineering need under the sun. However, GitHub also allows you to create your own custom actions suitable for your project's specific needs. Of course, if you feel that your custom GitHub Action can be useful to others, you may even publish it to GitHub Marketplace!

There are a number of reasons you may find yourself in need of a custom GitHub Action. For instance, your software project workflow may require some specific tasks that aren't already available. You may also want to optimize your workflow, reducing the wait time or resources needed to perform the tasks. Or you may need to integrate with external systems that are internal to your team or company. Finally, you may want to develop and offer custom GitHub Actions for profit.

## Custom GitHub Actions

While it's possible to build custom GitHub Actions in JavaScript, this approach imposes limits on what they can do. For example, they run directly on the runner and can only use binaries that are part of the runner image. The language itself may be a less-than-ideal choice for non-JavaScript developers.

You can also create composite actions, which consist of steps that use other actions, including shell scripts. However, if you prefer to write your action in any other language, GitHub allows you to supply the implementation as a [Docker container image](https://docs.github.com/en/actions/creating-actions/creating-a-docker-container-action); when used in a job step, the runner will execute the action in its own Docker container.

For best results, you should place your custom action in its own GitHub repository. This will make it easier to share and evolve as its own project.

What defines a custom GitHub Action is a YAML descriptor file named [action.yaml](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions), which must reside in the root of the action's repository. It contains both required and optional pieces of data, such as the action's name, description, and how it runs -- essentially its type.

Docker-based actions require a [Dockerfile](https://docs.github.com/en/actions/creating-actions/dockerfile-support-for-github-actions) to build the action's executable and package it into an image. In your action's descriptor, you can supply the path to your Dockerfile or the full Docker image URI. As you get started with developing your action, it may be convenient to specify the path to the Dockerfile; however, when you actually use your action in production, you'll benefit from building the image ahead of time and publishing it to a Docker registry (e.g., GitHub's own [Container Registry](https://ghcr.io)).

There are several mechanisms that your Docker-based action can use to [interact with the platform](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions). For instance, the platform will set up your container with environment variables that point to shared files for your action's output parameters, job summary, and even environment variables to set up for subsequent steps.

Custom GitHub Actions may utilize [input parameters](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputs) that the user can provide when calling the action in a job step. These parameters are passed to your container as command-line arguments. This is the easiest way to parameterize your action. Your action's descriptor file must define any input parameters that you wish to support, including their name, type, default value, and whether they're required.

## Custom GitHub Actions in Rust

If you haven't dipped your touch-typing fingers into Rust yet, you really owe it to yourself. [Rust](https://www.rust-lang.org/) is a modern programming language with features that make it suitable not only for systems programming -- its original purpose, but just about any other environment, too; there are frameworks that let your build web services, [web applications](https://www.arewewebyet.org/) including user interfaces, software for embedded devices, [machine learning](https://www.arewelearningyet.com/) solutions, and of course, command-line tools. Since a custom GitHub Action is essentially a command-line tool that interacts with the system through files and environment variables, Rust is perfectly suited for that as well.

Rust has a [rich ecosystem](https://crates.io/) of frameworks and libraries that let you read, parse, and manipulate text files, interact with cloud services and databases, and perform any other job that your project's development workflow may require. And because of its strong typing and tight memory management, you are much less likely to write programs that behave unexpectedly in production.

It is also quite easy to build a Rust program that links all of its dependencies into a single executable file. When you also [optimize your release build for size](https://doc.rust-lang.org/cargo/reference/profiles.html#opt-level), you end up with a small Docker container image that is quick to load and takes very little memory to do its job.

## Tips for building custom GitHub Actions in Rust

As you get started, consider the following tips to help you along:

### 1. Use the `act` tool to test-run your action

To speed up your development cycle, install and use the [act](https://github.com/nektos/act) tool to test-run your action directly in your development environment. This tool lets you invoke a GitHub workflow right on your local machine and will save you the round-trips of pushing each change to GitHub to see if it works.

### 2. Use debug messages

GitHub supports a [debug mode](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging) when executing workflows. This will cause any [debug message](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-debug-message) output to show up in the action's logs. This feature gives your users the ability to debug their workflows in a uniform manner.

### 3. Make your code easily testable

Write lots of unit tests, and use libraries that help you test filesystem operations, such as [assert_fs](https://crates.io/crates/assert_fs), as well as your command-line parsing, such as [assert_cmd](https://crates.io/crates/assert_cmd). This will save you a lot of headaches with regression bugs as you build out your action's functionality.

### 4. Keep command-line argument parsing simple

Fancy command-line argument parsing simply isn't necessary when building a custom GitHub Action. All input parameters are described and fed statically into your executable's command-line arguments. That means that all parameters will always be supplied in the order you define in `action.yaml`, regardless of their values, so ensure that your code knows how to handle empty strings and defaults.

### 5. Use GitHub Actions to automate your own development process

Not surprisingly, you can invoke your own action as part of your action's automated workflow! In addition to your regular Rust-based project workflow, add one that calls your action with a set of resources that you can use to test its functionality.

### 6. Build and publish your Docker image to ghcr.io for free

Take full advantage of what GitHub has to offer, for free:

1. Build and package your action as a Docker container image as part of your release workflow.
2. Publish it to GitHub's [Container Registry](https://ghcr.io), which you can set up for your action's repository.
3. Update your action's descriptor to use the desired version of your pre-built Docker image, which will greatly speed up its execution!

### 7. Use Alpine-based Rust builder for statically-linked executables

For the smallest possible Docker container images, build your action using an [Alpine Linux-based Rust builder](https://hub.docker.com/_/rust/tags?page=1&name=alpine). This image configures your Rust toolchain for static linking using the [musl library](https://musl.libc.org/), which produces single-file executables without any dynamic library dependencies. With this approach, your final Docker container image can consist of [your executable alone](https://hub.docker.com/_/scratch/), with no other files needed!

## Next steps

To learn more, sign up for this interactive project on how to [Build Custom GitHub Actions in Rust](https://www.educative.io/projects/build-custom-github-actions-in-rust) at Educative.io, which will take you step by step through creating your own custom GitHub Action in Rust!