I work on a small react based web project, a chrome/firefox extension built on plasmo.
Last week, the CICD pipeline started throwing dependency issues and so I have to put time & effort into fixing it. I figure with my recent love of nix, why not use nix for this build pipeline?
The output artifacts of the project are just static JS & manifest files which we send to the chrome/firefox web stores via cli, there are no docker images we need to worry about creating.
I already use a nix flake personally for development of the project on my machine, it works a treat.
I’m thinking the build pipeline can use a nixos docker image, which will in turn start the nix develop shell using that flake, and run the build process and emit those built artifacts.
This should ensure that the CI build environment works essentially the exact same as it does on my dev machine without docker.
I tried to look online for some guides and topics around using nix for CICD pipelines, but it seems the subject is not discussed much.
Is there anything obvious I’m missing?
Are there any guides you’d suggest I read before barking up this tree?
Have you done something similar in the past? If so, what are your impressions?
That’d certainly work if all you care about is dependency resolution, and you have things configured to do that correctly.
If in addition you would be interested in any of nix’ other features, and have less of a chance of getting it wrong, writing a derivation that builds your code and using nix build would be the “correct” way of doing it.
It’s hard to give any recommendations without knowing what’s involved in your build process. Nix has pretty decent support for npm-related stuff, though, and if you’re not using any other “real” build tool setting up a derivation should be completely trivial.
As for guides specifically for CI/CD, I’m not sure you’ll find much specifically for that. It’s basically just using nix in a script as you would anywhere else, except on someone else’s computer. The guides on https://nix.dev apply, but they don’t target builds for web-related stuff; without knowing your exact toolchain it’s hard to tell you where to look for that, though.
Interesting! I hadn’t really considered making a derivation, as I’m not packaging something for installation on other computers.
The value for me here is definitely in the consistent dependency resolution.
Using a development flake, and checking in the flake.lock gives me confidence that every nix-capable system will resolve those deps the same way (which is what broke and is arguably why I’m here rewriting this pipeline).
I’ll sleep on it, but not sure yet what other advantages there might be for me in using derivations.
For records’ sake, the gist of my CI pipeline now is (and it’s working):
{
description = "Nix Development Flake for Plasmo Browser Extension";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
# Node.js and pnpm
nodejs_20
pnpm
# TypeScript and other dev dependencies
typescript
nodePackages.typescript-language-server
nodePackages.eslint
# Firefox extension developer cli tool
# To debug firefox, you'll need two terminal sessions:
# 1. Run `nix develop`, then `pnpm plasmo dev --target=firefox-mv2`
# 2. Run `nix develop`, then `cd` into the build directory and run `web-ext run`
# This will open a new firefox window with the extension loaded, and will automatically reload the extension when you make changes
web-ext
];
shellHook = ''
clear
echo "----------------------------------------"
echo "🚀 Development Dependencies for Browser Extension Development"
echo "📦 Using pnpm as package manager"
echo "----------------------------------------"
echo "📊 Installed Versions:"
echo " Node.js: $(node --version)"
echo " pnpm: $(pnpm --version)"
echo " TypeScript: $(tsc --version)"
echo " ESLint: $(eslint --version)"
echo "----------------------------------------"
'';
};
}
);
}
I’d say so far the only annoyance is that I had to break out the build script into a bash file because I can’t really write it in the CI yaml without it becoming gross.
I might try to revisit how that’s working, because I still dont love that bash script with the embedded commands in a string
Looking at your setup, it could help keep your builds repeatable across systems. npm is full of projects that download stuff from the internet at build time. What you have right now could very well fail to build on a system with a different dynamic linker, and there is not much to guarantee future linkers will be compatible with old versions if you need to dig through history.
You could replace your build script with a flake output and just running nix build. Other people who build your project could then also just use nix build and be reasonably certain they’d get the exact same build env and ultimately outputs.
Depending on the nix builder involved, it could also give you the devshell for free, pre-seeded with your dependencies.
That said, I’m not sure if it’d be trivial, afaik pnpm’s lockfile isn’t compatible with npm or yarn’s, and I’m not sure if tooling to import it into nix exists.
One thing we do is we have a directory tooling/bin which we add to PATH in the shellHook.
The script part in the .gitlab-ci.yml then could look like this:
nix develop --command ci-build-extension.sh
And the scripts content looks like this:
#!/usr/bin/env bash
pnpm install --frozen-lockfile --ignore-scripts=false --verbose &&
mkdir -p build &&
pnpm run build &&
ls build
We use this mostly for linters etc. but as well for building and pushing container images (they use nix build) because it is easier to write the scripts in a separate file than having multiple lines in YAML.