Reproducibility with prefetch-npm-deps and git dependencies

Situation

I have a JS application that uses a dependency from GitHub instead of npm and is causing non-reproducible builds between my local machine and my CI server. I’d like it to not do that.

The dependency in question is a pre-built dependency svelte-select, pulled in through my package.json as:

{
  "devDependencies": {
    "svelte-select": "github:xeals/svelte-select#publish"
  }
}

I’m using Gitea Actions for CI (OSI container-based, hitting the same issue with both Docker and Podman). The system is mostly compatible with GitHub Actions semantically, with the following jobs:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: https://gitea.com/actions/checkout@v3
      - uses: https://github.com/cachix/install-nix-action@v25
      - uses: https://github.com/cachix/cachix-action@v14
        with:
          name: xeals
          signingKey: ${{ secrets.CACHIX_SIGNING_KEY }}
      - run: nix build

The package is built using a fairly standard buildNpmPackage spec:

  frontend = buildNpmPackage {
    pname = "xxx-frontend";
    inherit version src;

    sourceRoot = "${src.name}/frontend";

    npmDepsHash = "sha256-Zrke7Uf1HA5gXsOIPAoXGT5gY8Z2+oMfJGcqEM3WuB4=";
    makeCacheWritable = true;
    NODE_ENV = "production";
    NODE_OPTIONS = "--openssl-legacy-provider";

    npmInstallFlags = [ "--include=dev" ];
  };

Findings

Building the package as you’d expect on my host is fine. Running the CI job fails trying to build the npm dependencies derivation:

error: hash mismatch in fixed-output derivation '/nix/store/kq87zimay4kgqpjb34gb9z3qbjn30iy8-xxx-frontend-0.0.1-npm-deps.drv':
         specified: sha256-Zrke7Uf1HA5gXsOIPAoXGT5gY8Z2+oMfJGcqEM3WuB4=
            got:    sha256-MUGcy3fdrYgT9DslG+ia49VTquNRmOb8hmGGOMv80lc=

With some playing around locally, I’ve inspected the cacaches that prefetch-npm-deps tries to generate and verified that it’s not consistent.

$ nix run --inputs-from . nixpkgs#prefetch-npm-deps frontend/package-lock.json cache.host
$ # add the equivalent line to the CI job, then run it locally
$ nix run nixpkgs#gitea-actions-runner -- exec push --container-opts "-v $PWD/cache.ci:$PWD/cache.ci"
$ diff -r cache.*
Only in cache.ci/_cacache/content-v2/sha512/6c: fc
Only in cache.host/_cacache/content-v2/sha512/cd: 24
diff '--color=auto' -r cache.ci/_cacache/index-v5/84/61/e48f6b6591021d3e88365d8fc033054c722a3b98674c275d369924a8224c cache.host/_cacache/index-v5/84/61/e48f6b6591021d3e88365d8fc033054c722a3b98674c275d369924a8224c
1c1
< 793506d4fcf1dadc46881476123bb9d1ef2e62d8      {"key":"make-fetch-happen:request-cache:https://codeload.github.com/xeals/svelte-select/tar.gz/93e6000f938ecd7b7f7b79c780a942c26a36982f","integrity":"sha512-bPz7sRaT+mXhJVR7YhxRxvQ6E05yKrHI5CoUAE3fmgpCf443FjwuGqVDtpgRvGlCfSQZ2BB8TsMmp/0vNmCwbA==","time":0,"size":27886,"metadata":{"url":"https://codeload.github.com/xeals/svelte-select/tar.gz/93e6000f938ecd7b7f7b79c780a942c26a36982f","options":{"compress":true}}}
\ No newline at end of file
---
> a7718d2bc51ede9601b2d534d42ea631732b989e      {"key":"make-fetch-happen:request-cache:https://codeload.github.com/xeals/svelte-select/tar.gz/93e6000f938ecd7b7f7b79c780a942c26a36982f","integrity":"sha512-zSS5Qf3ghAHvsdeURD0bS1INQw7HB5cXGBJJ4p9WmrlHvdwgy8ry51o4EF+4+Puy6SqFR7FvcwDopmHyIt228A==","time":0,"size":27884,"metadata":{"url":"https://codeload.github.com/xeals/svelte-select/tar.gz/93e6000f938ecd7b7f7b79c780a942c26a36982f","options":{"compress":true}}}
\ No newline at end of file

The integrity checksum and size fields are the only differences, and while they’re not consistent between local and CI builds, they are reproducible within those environments.

Attempted solutions

I thought that this issue was a re-emergence of something like npm/cli#2846, but I don’t know the difference between environments and enough about how tar/gzip work to know if it can be a perceived architecture issue.

The issue appears to be independent of dirty source trees or configs being sourced outside the sandbox somehow, as best as I can tell (attempted building in tempdir, as a different user, and on a different physical machine). It crops up only on the container CI build.

My attempted workaround was to vendor the dependency as a tarball and avoid a possible integrity mismatch, but prefetch-npm-deps doesn’t support file: dependencies, so I believe I’d have to move to node2nix or change build systems entirely. Vendoring it as source code/a git submodule (as a dependency) runs into the same issue while also messing up my package.json. I’d like to avoid vendoring it directly into my source code for the same reason.

I’ve considered scanning nixpkgs for existing packages that might have solved this issue, but that would be very time consuming.

Could you upload a reproducible example, or at least the package.json and package-lock.json?

I’ve created a minimal reproduction repository here.