Github Actions is a handy CICD solution integrated directly into Github. This tight integration not only provides an obvious solution to get started with automation concerns related to a project but also provides a convenient dashboard for any action you might want your project to perform. It’s a great step towards having a single-pane-of-glass approach for your project.
One common concern when writing CICD scripts is how to keep these DRY. No, I’m not talking about humidity issues or leaky pipes here — DRY, or Don’t Repeat Yourself, philosophy in software design is the concept of minimizing repetition. These strategies tend towards more maintainable code and an easier understanding of complex systems.
Github Actions provides a couple of features to assist in this regard, namely reusable workflows, and custom actions. Today we’ll discuss how these function at a high level, and when you might choose one over the other.
Actions Components: High-Level Overview
First, let’s refresh on the high-level components of Github Actions:
The scripts you execute are known as ‘workflows’. Workflows are triggered by some kind of event (or action!) such as a pull request, merge, manual trigger, or time event.
Each workflow contains one or more jobs. The defined jobs within a workflow are then executed in the order you define within the workflow. For instance, jobs can be structured to be run parallel or after another job completes. Each job is effectively independent and does not share state by default with other jobs. You should assume that each job is run on an entirely separate system.
Each job contains a series of steps. These steps run in order and within the same context. You can assume that changes made in one step will be represented in the next step.
A reusable workflow is a pre-defined Github Actions workflow that can be called from another workflow. The reusable workflow effectively works like a template in which we define the set of inputs and end up with a fully rendered workflow.
The reusable workflow itself contains one or more jobs, complete with steps, and any other syntax you normally could define for a workflow. You can, for instance, create a reusable workflow that defines a particular docker image you wish to run a job in, and on what type of system the execution should occur on. Since the complicated parts can be packaged within the reusable workflow, the calling workflow that references it can stay neat and tidy.
While the definition of the reusable workflow is quite flexible, executing the workflow is not so flexible.
The caller workflow that refers to the reusable workflow can only define a subset of keywords in the job calling the workflow: name, uses, with, secrets, needs, if, and permissions. Any other feature is not supported. For instance, if you wish to define an environment variable using the ‘env’ keyword, you are out of luck.
Due to this gotcha, reusable workflows are best for circumstances where you have a complex workflow that only requires a limited set of inputs in order to define how you wish to execute it.
Custom actions can be defined in a number of ways, but to keep things simple we will just look at what’s known as ‘composite actions’. Composite actions are actions that are defined in YAML using the same kind of syntax we use to build workflows.
As opposed to a reusable workflow that is called within a job of a calling workflow, composite actions are called in the context of steps. This provides a bit of flexibility of execution, at the expense of compactness.
The composite action itself has a similar, but limited syntax to a typical workflow. These just define a series of steps, inputs, and outputs. Composite actions have no concept of higher-level concerns such as jobs, permissions, containers, or the like.
Instead, all job level concerns are defined in the calling workflow itself. The composite action is simply a step within a job for that workflow. The upside is this allows a lot of flexibility in how the actions are performed. The calling workflow itself can function as per normal. Environment variables defined in the job will be available in the composite action as expected, for instance.
The downside is that we can capture less complex configurations with this method, leading to a more verbose calling workflow. The execution of the composite action in logs will appear as a single step within the calling workflow, which may be positive or negative, depending on expectations.
Furthermore, there are some syntax restrictions with the composite action itself. For instance, you must specify the shell you wish to use when defining a run step. Also, the composite action must be named action.yml.
Composite actions would lend themselves to be most useful where you have a specific set of steps that must be executed, but you may need to execute these in a flexible manner.
Both in the case of reusable workflows and composite actions, there are some limitations in how these can be organized and accessed, particularly if you use private repositories. Currently, both of these methods require you either to use a public repository to host the scripts or that the scripts are within the same repository where they will be referenced.
For some organizations this may be a deal-breaker. At the very least this may lead to design considerations in how you structure your CICD tooling.
At Doximity, we have found composite actions to be a good middle ground between flexibility and compactness. Overall we have found the Github Actions service itself to provide great visibility into our CICD processes. Features and improvements are coming at a feverish clip, perhaps Github will soon add the ability to use composite actions across private repos.
Be sure to follow @doximity_tech if you'd like to be notified about new blog posts.