If you manage Nix-based projects, you probably love the reproducibility, but you probably hate the boilerplate. I created a tool called Soonix to address this issue. It generates configuration files such as renovate.json and .gitlab-ci.yml directly from Nix code, ensuring that my repositories stay consistent.
However, automating dependencies with Renovate introduced a tricky synchronisation problem.
The Setup
I use Soonix alongside Nix-GitLab-CI. This combination automatically generates a .gitlab-ci.yml file containing bootstrapping code including it’s GitLab CI/CD component. Since CI/CD components cannot access their own version (like when you include component@1.2.3 it cannot access this version, which is an issue if the component uses it’s own docker image which is also versioned), there is an “input” for “version”, which has to be the same as the component version (or rather should, here be dragons if not):
include: - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@3.0.0 inputs: version: 3.0.0By using Soonix, Nix-GitLab-CI can define this in Nix code, which then gets converted to YAML. By simply having a VERSION file in Nix-GitLab-CI, we can use builtins.readFile from Nix, to get the current version (depending on the flake input) and add it to the config:
include = [ { component = "${config.componentUrl}@${config.componentVersion}"; inputs = { version = config.componentVersion; } // config.componentInputs; }];# [...]Here config.componentVersion can be overridden, but defaults to trimWith {start = true; end = true;} (builtins.readFile ./VERSION).
The Problem
When Renovate now tries to update the flake inputs and GitLab CI/CD component versions, it results in a change similar to this:
--- a/.gitlab-ci.yml+++ b/.gitlab-ci.yml@@ -1,4 +1,4 @@ include:- - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@2.1.0+ - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@3.0.0 inputs: version: 2.1.0The version of the input (thus the docker image) is not updated. While it’s certainly possible to fix this using Renovate’s RegEx manager (that’s what I used before), it’s pretty limiting and since we already generate these files it’s inefficient to then edit them using RegExes.
So, we just have to run the Soonix hook in Renovate and be done?
Yeah! Kinda :D
Let’s try adding nix run .#soonix:update to the renovate.json’s postUpgradeCommands:
warning: binary cache 'https://cache.nixos.org' is for Nix stores with prefix '/nix/store', not '/tmp/containerbase/cache/nix/store'error: hash mismatch in fixed-output derivation '/tmp/containerbase/cache/nix/store/8ygpz0xfscxbn9wyhgsn752na5rvqsfh-gettext-0.25.1.tar.gz.drv': specified: sha256-dG+VXULXHrac52OGnLkmgvCaQGZSjQGLbKej9ICJoIU= got: sha256-L6XF1fSV7m2aceK9vkumy72fHPeFN/DzOIDtzuvLuDo=error: Cannot build '/tmp/containerbase/cache/nix/store/9zq2gd1ccafzqarrba9zyxmsi9nigk7g-gettext-0.25.1.drv'. Reason: 1 dependency failed. Output paths: /tmp/containerbase/cache/nix/store/4akkyicl0vlz4ppiibi1hbz6r4ww0p6j-gettext-0.25.1-info /tmp/containerbase/cache/nix/store/67h0l1142rx02cmghawz9flbpmk0v920-gettext-0.25.1-doc /tmp/containerbase/cache/nix/store/dhgii7f71wj1217pp57vxsy6dl6in531-gettext-0.25.1 /tmp/containerbase/cache/nix/store/xaa94ldnnxwb4m1mfq4146dahr9ybjr7-gettext-0.25.1-manerror: Cannot build '/tmp/containerbase/cache/nix/store/wc4dy4ygr2lqhwiifrnqmfn0xbwlgsw8-gettext-0.25.1.drv'.# [...] a bunch of more lines like aboveerror: Cannot build '/tmp/containerbase/cache/nix/store/asw70crc06yxlfiicch3va70r8a7mc7n-stdenv-linux.drv'. Reason: 1 dependency failed. Output paths: /tmp/containerbase/cache/nix/store/wmzmdn337pywyirwv3ykkbpc33h2rw3p-stdenv-linux# [...]Uh oh, that brings back some memories tinkering with Nix in root-less scenarios where I tried to not use /nix.
Well, let’s see why Nix tries to use this path:
docker run --rm -it renovate/renovate:42.11.0 /bin/bash$ install-tool nix 2.32.2 # what renovate does under the hood$ cat $(which nix)# [...] some other stuffexport NIX_STORE_DIR=/tmp/containerbase/cache/nix/store NIX_DATA_DIR=/tmp/containerbase/cache/nix/data NIX_LOG_DIR=/tmp/containerbase/cache/nix/log NIX_STATE_DIR=/tmp/containerbase/cache/nix/state NIX_CONF_DIR=/tmp/containerbase/cache/nix/conf/opt/containerbase/tools/nix/2.32.2/bin/nix "$@"Oh, yeah, it just forces Nix to have it’s store at /tmp/containerbase/cache/nix/store, but Nix doesn’t really like that since now it cannot use https://cache.nixos.org to substitute derivations. Thus the build takes an eternity and ultimately fails since stuff couldn’t be built or hash mismatches occur.
The Solution
After trying all kinds of combinations of Nix with proot and others, I decided to just try nix-portable, surely they are better than me at using proot. After quickly downloading it with curl and making it executable, I tried it in the renovate container and it actually worked ootb. Nice.
Since install-tool from containerbase doesn’t support nix-portable and I didn’t want to specify the installation commands in all my renovate.json files, I ended up modifying the docker entrypoint. I run Renovate as a Kubernetes CronJob, so there you have to change command and args:
command = ["sh"];args = [ "-c" # sh '' mkdir -p /home/ubuntu/.local/bin curl -L https://github.com/DavHau/nix-portable/releases/latest/download/nix-portable-$(uname -m) > /home/ubuntu/.local/bin/nix-portable chmod +x /home/ubuntu/.local/bin/nix-portable exec /usr/local/sbin/renovate-entrypoint.sh renovate ''];Probably a good idea to pin nix-portable instead of using latest.
Also setting this env (or using the Renovate config file, whatever you use):
RENOVATE_ALLOWED_COMMANDS="nix-portable nix run \.#.*"Now, just add nix-portable nix run .#soonix:update to the postUpgradeCommands and it works! Every Renovate PR/commit now includes the up-to-date generated files. And since Nix caches stuff, I just let it run for every update instead of once per branch (default setting in Renovate), just the first run takes a bit longer.
Update
I run this setup for a couple of months now, one issue I found was that merging multiple renovate MRs is now a struggle. Since every MR changes the hash, they can only be merged one after another (waiting for renovate to rebase them every time).
So some automation is required to merge these MRs together like in a merge train, ignore the conflict on the hash and then run nix run .#soonix:update afterwards. That’s still an open TODO :)