# Automatic flake updates with GitLab CI

Table of Contents

Introduction

Nix flakes are a mechanism for declaring the inputs/outputs of a project with a standard structure. They enable pinned inputs for dependencies like nixpkgs, which is important for reproducibility and a crucial component of using Nix in CI/CD environments. They are technically experimental but in practice used by a very large chunk of the userbase.

This post discusses an automatic update mechanism for them based on GitLab’s scheduled pipelines. The main application of such a mechanism is automated updates for components built/deployed in CI jobs, like container images.

Motivation

A few of my projects publish container images built with nixpkgs’s dockerTools. These images generally try to only contain the compiled binaries of the software itself.

However, in some cases they also vendor third-party dependencies from nixpkgs, like Isabelle. These cases benefit from automatic rebuilds to propagate updates to consumers, both for security and feature updates.

You can also use the same mechanism to provide automatically rebuilt container images for use in CI. If you, e.g., want to inlude an extra Nix cache or have specific tooling available in the image.

General approach

The plan is to start a scheduled job, which:

  1. update the Nix flake inputs
  2. (optional) performs validation
  3. commit & push the changes
  4. starts a new pipeline for the commit from 3.

Scheduled jobs

GitLab scheduled pipelines are a mechanism for running a pipeline on regular intervals. In practice this means a pipeline will be started in the same manner as a cron job at some defined time interval, like hourly or daily. This can be used to automate nightly release tags or in our case automatic updates.

Please refer to the GitLab documentation on how to create a pipeline schedule. You can restrict jobs to only run in scheduled pipelines via rules (read more about this topic in the GitLab docs):

# this job will only run when a pipeline is initiated by schedule,
# this can be either on the regular interval or by manually forcing a schedule pipeline run
scheduled job:
script:
- echo "Perform scheduled work"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"

Updating the Nix flake inputs

Updating Nix flake inputs is normally done with: nix flake update

However, in some cases you might want to detect which inputs were actually updated. We will start writing a small helper for this:

update.nix
{ writeShellApplication, inputs ? ["nixpkgs"], lib, nix, coreutils, gnugrep }:
writeShellApplication {
name = "update-flake";
runtimeInputs = [ coreutils gnugrep nix ];
script = ''
${lib.toShellVar "inputs" inputs}
declare -a updated_inputs=()
for input in "''${inputs[@]}"; do
if nix flake update "$input" |& tee /dev/stderr | grep -i updated; then
updated_inputs+=("$input")
fi
done
if [ ''${#updated_inputs[@]} -eq 0 ]; then
echo "No updates for inputs found"
exit 0
fi
echo "Updated ''${updated_inputs[@]} inputs"
'';
}

A brief explanation of this bash script:

  1. declare -a updated_inputs / ${lib.toShellVar "inputs" inputs}: declare two bash arrays, one empty one for the list of updated inputs and another one holding the name of all inputs we want to automatically update.
  2. for inputs ...: go over each input, try to update it and check if nix found an updated version.
  3. if [ ''${...} ]: Early exit if no updates were found, i.e., no updated inputs were recorded in updated_inputs
  4. echo "Updated ...": If updates were found, return their names

One could simplify this script to a single nix flake update but in my experience you generally want to automatically update a subset of your inputs.

Perform validation

After an update you likely want to run your unit or integration test suite (you have one, right?). For NixOS configuration repositories, this is usually at least nix flake check.

Depending on your project, add something like nix develop -c make test after the line echo "Updated ..." and early exit with an error iif your test suite failed.

While, e.g., a stable NixOS release channel mostly do not ship too many breaking changes it can still happen. Especially if security updates force a maintainers hand. If you depend on nixos-unstable then also definitely run at least nix flake check after every update.

Commit & push the changes

Commiting changes in a pipeline is relatively easy, just use git. GitLab even helps you here a bit by prepoluting the environment variables GITLAB_USER_LOGIN and GITLAB_USER_EMAIL. This ensures the commit can be attributed to the person who set up a scheduled pipeline.

As an extension of the helper above this might look like:

{
writeShellApplication, jq, nix, coreutils, gitMinimal,
inputs ? [ "nixpkgs" ],
lib, gnugrep,
}:
writeShellApplication {
name = "update-flake";
runtimeInputs = [ jq coreutils gitMinimal gnugrep nix ];
text = ''
# [snip] update part from the previous section
# build a human readable commit title/message, this should contain the updated inputs and revs
# having a good commit message makes it easier to track down an update later
commit_title="''${updated_inputs[*]}"
commit_title="''${commit_title// /, }"
commit_title="flake(inputs): update for $commit_title"
commit_message=""
for input in "''${updated_inputs[@]}"; do
# extract updated rev from flake lock file
commit_message="$commit_message$input: $(jq -r .nodes.\""$input"\".locked.rev flake.lock)"$'\n'
done
commit_message=''${commit_message%$'\n'} # remove trailing newline from last item
git commit -a -m "$commit_title" -m "$commit_message"
# [snip] here should be your validation, nix flake check -L
git push
'';
}

To use git push in a pipeline, you will first have to enable git write access for CI job token. Please refer to the GitLab documentation for a detailed guide. By default, this should be disabled for your project, so it will require some manual action from a maintainer/owner.

You will also at this point likely want to restrict this job to your default branch, this can be done with rules like:

nix update:
stage: update
script:
- ...
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "schedule"
variables:
# below is helpful but not always required
GIT_STRATEGY: clone

Automatic updates for non-default branches is of course also possible. A suitable approach here might be a matrix for the relevant branches.

Start a new pipeline

The final step is starting a new pipeline with your updated changes. In general one would expect a git push to trigger the pipeline already but a commit from a scheduled job (or a CI job in general), will not trigger a pipeline. As such we have to use the API to trigger a new pipeline ourselves.

For this step we will rely on the GitLab CLI, glab, to use our job token for starting a downstream pipeline. glab is also packaged in nixpkgs so we can just tag it at the end of our helper:

{
writeShellApplication, jq, nix, coreutils, gitMinimal,
inputs ? [ "nixpkgs" ],
lib, gnugrep, glab,
}:
writeShellApplication {
name = "update-flake";
runtimeInputs = [ jq coreutils gitMinimal gnugrep nix glab ];
text = ''
# [snip] update part from the previous section
# [snip] build the commit message & commit
# [snip] here should be your validation, nix flake check -L
git push
# auth snippet from https://gitlab.com/gitlab-org/cli#ci-job-token
glab auth login --job-token "$CI_JOB_TOKEN" --hostname "$CI_SERVER_HOST" --api-protocol "$CI_SERVER_PROTOCOL"
GITLAB_HOST="$CI_SERVER_URL" glab ci run-trig -R "$CI_PROJECT_PATH" -b "$CI_DEFAULT_BRANCH"
'';
}

And that’s it, to integrate this into a pipeline you might use:

nix update:
stage: build
script:
- nix run .#ci-update-flake # package with the helper from above
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "schedule"
variables:
GIT_STRATEGY: clone

At this point you should be ready to enable your scheduled pipeline. Please note that you likely can trigger the scheduled pipeline manually via the web UI, in case you need to debug the validation or update logic.

Conclusion

This post discussed automatic updates for a nix flake via GitLab CI with a bash helper that relies on tooling from nixpkgs. The code is based on my work on proveit.nix and a derivative of my automated container images builder.

Honorable mention: Renovate

Rolling a custom update script for Nix in GitLab can also be skipped directly by using Renovate. You can leverage the same scheduled pipeline mechanism through the GitLab Renovate Runner template. This also notably enables automatic updates for other package managers, like npm or devbox.

Renovate supports Nix flakes, although it can be a bit flaky. This notably will also not just commit to your repo but instead open an automatically updated merge request instead. If you want to review updates beforehand and/or do not want to commit to your primary branch directly this is a good alternative.

I have used this successfully in the past but switched to my own solution here to simplify the projects overall workflow. There also were some other issues around testing/validation that made a custom update script easier to maintain.

Business cat avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.


More Posts