Anyone who gets into 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, you save time for your future self, 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 might be set up right at the start, high-value projects tend to evolve in unique ways that eventually require a lot of customization.
If you’ve used GitHub while working on a software project, chances are you’ve seen GitHub 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 is tightly integrated into your software development life cycle, where your code lives and much of your daily product development and collaboration happens. It’s really intuitive and easy to grasp. It uses domain language 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, GitHub Actions is free to use, and GitHub even provides free computer infrastructure to run your workflows!
In addition to a set of commonly required actions provided directly by the platform itself, GitHub supports a rich marketplace of third-party actions that seem to cover any engineering need. 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 can even publish it to GitHub Marketplace!
There are a number of situations where you might find yourself in need of a custom GitHub action. For instance, your software project workflow might require some specific tasks that aren’t already available. You might also want to optimize your workflow, reducing the wait time or resources needed to perform the tasks. Or you might need to integrate with external systems that are internal to your team or company. Finally, you might want to develop and offer custom GitHub actions for profit.
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 might 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. When it’s 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`, 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 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 might 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).
There are several mechanisms that your Docker-based action can use to interact with the platform. 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 can utilize input parameters 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.
Rust 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 you build web services and web applications, including user interfaces, software for embedded devices, machine learning solutions, and of course, command-line tools. A custom GitHub action is essentially a command-line tool that interacts with the system through files and environment variables, and Rust is also perfectly suited for that.
Rust has a rich ecosystem of frameworks and libraries that let you read, parse, and manipulate text files, interact with cloud services and databases, and perform any other job your project’s development workflow might 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, you end up with a small Docker container image that is quick to load and takes very little memory to do its job.
As you get started, consider the following tips to help you along:
To speed up your development cycle, install and use the `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. It’ll save you the round-trips of pushing each change to GitHub to see if it works.
GitHub supports a debug mode when executing workflows. This will cause any 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.
Write lots of unit tests, and use libraries that help you test filesystem operations, such as `assert_fs`, as well as your command-line parsing, such as `assert_cmd`. This will save you a lot of headaches with regression bugs as you build out your action’s functionality.
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, you must ensure that your code knows how to handle empty strings and defaults.
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.
Take full advantage of what GitHub has to offer for free:
Build and package your action as a Docker container image as part of your release workflow.
Publish it to GitHub’s Container registry, which you can set up for your action’s repository.
Update your action’s descriptor to use the desired version of your prebuilt Docker image, which will greatly speed up its execution!
For the smallest possible Docker container images, build your action using an Alpine Linux-based Rust builder. This image configures your Rust toolchain for static linking using the musl library, which produces single-file executables without any dynamic library dependencies. With this approach, your final Docker container image can consist of your executable alone, with no other files needed!
To learn more, sign up for this interactive project on how to Build custom GitHub actions in Rust at Educative.io that will take you step by step through creating your own custom GitHub action in Rust!
Free Resources