I maintain many repositories using Gitea Actions, and I’ve run into a lot of pipeline failures due to the smallest of issues. Most commonly it is whitespace differences in YAML files that cause unexpected parsing or complete failures. Recently, I was helping someone debug a workflow where a copy and paste from a different workflow brought in spaces for some lines instead of tabs leading to the pipeline to fail.
The Problem with YAML
YAML is extremely sensitive to whitespace, and that can lead to subtle issues that are hard to debug. Especially when you can’t see the differences visually between tabs and spaces.
The Solution: Nix
In my case, as is the case with all my problems, the solution was to use Nix. Recently, I was using Terranix to manage some infrastructure (which is a tool that translates Nix to JSON for Terraform), I realized a similar approach could work for Gitea Actions. Both JSON and YAML represent data as key-value pairs, making them easily convertable between each other, meaning I can use the same approach to convert Nix to JSON to YAML.
Benefits of Using Nix for Workflows
- Elimination of whitespace-related errors
- Nix features, like conditionals, loops, and functions
- Improved code reuse and maintainability
- Ability to split workflows into smaller, manageable components
Implementing Nix-based Workflows
Step 1: Creating a Basic Step Function
The most common component of a workflow is a step. So we could start by defining a function that creates a step.
# steps.nix
let
mkStep = { name, uses, run, with' }:
let
step = {
name = if name != null then name else null;
uses = if uses != null then uses else null;
run = if run != null then run else null;
with = if with' != null then with' else null;
};
in
builtins.removeAttrs (builtins.filterAttrs (a: v: v != null) step) [ "name" "uses" "run" "with" ];
in
mkStep
This mkStep
function creates a step object and removes any null fields, resulting in cleaner YAML output.
Note: In Nix,
with
is a reserved keyword in Nix, so I usedwith'
instead.
Step 2: Creating a Checkout Function
Building on the mkStep
function, the most common step in every workflow is the checkout step. So we can create a function to create a checkout step which uses the mkStep
function.
# steps.nix
# ... keep the previous code and add the new `mkCheckout` function
mkCheckout = { name, uses ? "actions/checkout@v4", with' }:
mkStep {
name = name;
uses = uses;
with' = with';
};
# Export both functions
{
mkStep = mkStep;
mkCheckout = mkCheckout;
}
This approach can be extended to other common actions, and potentially even generating functions automatically from action.yaml
files.
Step 3: Creating a Workflow
Using the functions above, we can create a complete workflow.
# workflow.nix
let
steps = import ./steps.nix { };
mySteps = [
(steps.mkCheckout { with = { fetch-depth = 0; }; })
(steps.mkStep { run = "echo 'Hello, World!'" })
# More steps could be added here
];
in
{
name = "Example Workflow";
on = "push";
jobs = {
build = {
runs-on = "ubuntu-latest";
steps = mySteps;
};
};
}
Generating the Gitea Actions YAML Workflow
To convert our Nix workflow to YAML, we can use the following command:
nix-instantiate --json ./workflow.nix | yq eval -P - > example.yaml
This command generates the YAML file (assuming you have the yq
command installed).
Future Possibilities
The benefits can be immediatly seen, especially for larger projects. For example, in the Gitea project, there are two workflows, one to build release artifacts for a nightly release, and one to build release artifacts for a tagged release. Both of these workflows are very similar, and so arguments and conditionals could be used to reduce output by half.
Nix Flakes could potentially be used to define the inputs for an Action’s step, and then be used by the workflow. This would allow for a more declarative and modular approach to workflows.
Conclusion
By using Nix, I can now write workflows in a more declarative and modular way, and all of my pain points with YAML have been resolved.
Editor’s note: The Nix above hasn’t been tested, and is just a rough draft of what I’m thinking. If you do attempt this as an approach I’d love to hear, and update this post with any feedback you might have. I am also a maintainer of the Gitea project.