lol

umami: init at 2.19.0; nixos/umami: init (#380249)

authored by

Niklas Hambüchen and committed by
GitHub
436a8a11 3ab60cda

+623
+2
nixos/doc/manual/release-notes/rl-2511.section.md
··· 23 23 24 24 - [Fediwall](https://fediwall.social), a web application for live displaying toots from mastodon, inspired by mastowall. Available as [services.fediwall](#opt-services.fediwall.enable). 25 25 26 + - [umami](https://github.com/umami-software/umami), a simple, fast, privacy-focused alternative to Google Analytics. Available with [services.umami](#opt-services.umami.enable). 27 + 26 28 - [FileBrowser](https://filebrowser.org/), a web application for managing and sharing files. Available as [services.filebrowser](#opt-services.filebrowser.enable). 27 29 28 30 - Options under [networking.getaddrinfo](#opt-networking.getaddrinfo.enable) are now allowed to declaratively configure address selection and sorting behavior of `getaddrinfo` in dual-stack networks.
+1
nixos/modules/module-list.nix
··· 1690 1690 ./services/web-apps/szurubooru.nix 1691 1691 ./services/web-apps/trilium.nix 1692 1692 ./services/web-apps/tt-rss.nix 1693 + ./services/web-apps/umami.nix 1693 1694 ./services/web-apps/vikunja.nix 1694 1695 ./services/web-apps/wakapi.nix 1695 1696 ./services/web-apps/weblate.nix
+316
nixos/modules/services/web-apps/umami.nix
··· 1 + { 2 + config, 3 + lib, 4 + pkgs, 5 + ... 6 + }: 7 + let 8 + inherit (lib) 9 + concatStringsSep 10 + filterAttrs 11 + getExe 12 + hasPrefix 13 + hasSuffix 14 + isString 15 + literalExpression 16 + maintainers 17 + mapAttrs 18 + mkEnableOption 19 + mkIf 20 + mkOption 21 + mkPackageOption 22 + optional 23 + optionalString 24 + types 25 + ; 26 + 27 + cfg = config.services.umami; 28 + 29 + nonFileSettings = filterAttrs (k: _: !hasSuffix "_FILE" k) cfg.settings; 30 + in 31 + { 32 + options.services.umami = { 33 + enable = mkEnableOption "umami"; 34 + 35 + package = mkPackageOption pkgs "umami" { } // { 36 + apply = 37 + pkg: 38 + pkg.override { 39 + databaseType = cfg.settings.DATABASE_TYPE; 40 + collectApiEndpoint = optionalString ( 41 + cfg.settings.COLLECT_API_ENDPOINT != null 42 + ) cfg.settings.COLLECT_API_ENDPOINT; 43 + trackerScriptNames = cfg.settings.TRACKER_SCRIPT_NAME; 44 + basePath = cfg.settings.BASE_PATH; 45 + }; 46 + }; 47 + 48 + createPostgresqlDatabase = mkOption { 49 + type = types.bool; 50 + default = true; 51 + example = false; 52 + description = '' 53 + Whether to automatically create the database for Umami using PostgreSQL. 54 + Both the database name and username will be `umami`, and the connection is 55 + made through unix sockets using peer authentication. 56 + ''; 57 + }; 58 + 59 + settings = mkOption { 60 + description = '' 61 + Additional configuration (environment variables) for Umami, see 62 + <https://umami.is/docs/environment-variables> for supported values. 63 + ''; 64 + 65 + type = types.submodule { 66 + freeformType = 67 + with types; 68 + attrsOf (oneOf [ 69 + bool 70 + int 71 + str 72 + ]); 73 + 74 + options = { 75 + APP_SECRET_FILE = mkOption { 76 + type = types.nullOr ( 77 + types.str 78 + // { 79 + # We don't want users to be able to pass a path literal here but 80 + # it should look like a path. 81 + check = it: isString it && types.path.check it; 82 + } 83 + ); 84 + default = null; 85 + example = "/run/secrets/umamiAppSecret"; 86 + description = '' 87 + A file containing a secure random string. This is used for signing user sessions. 88 + The contents of the file are read through systemd credentials, therefore the 89 + user running umami does not need permissions to read the file. 90 + If you wish to set this to a string instead (not recommended since it will be 91 + placed world-readable in the Nix store), you can use the APP_SECRET option. 92 + ''; 93 + }; 94 + DATABASE_URL = mkOption { 95 + type = types.nullOr ( 96 + types.str 97 + // { 98 + check = 99 + it: 100 + isString it 101 + && ((hasPrefix "postgresql://" it) || (hasPrefix "postgres://" it) || (hasPrefix "mysql://" it)); 102 + } 103 + ); 104 + # For some reason, Prisma requires the username in the connection string 105 + # and can't derive it from the current user. 106 + default = 107 + if cfg.createPostgresqlDatabase then 108 + "postgresql://umami@localhost/umami?host=/run/postgresql" 109 + else 110 + null; 111 + defaultText = literalExpression ''if config.services.umami.createPostgresqlDatabase then "postgresql://umami@localhost/umami?host=/run/postgresql" else null''; 112 + example = "postgresql://root:root@localhost/umami"; 113 + description = '' 114 + Connection string for the database. Must start with `postgresql://`, `postgres://` 115 + or `mysql://`. 116 + ''; 117 + }; 118 + DATABASE_URL_FILE = mkOption { 119 + type = types.nullOr ( 120 + types.str 121 + // { 122 + # We don't want users to be able to pass a path literal here but 123 + # it should look like a path. 124 + check = it: isString it && types.path.check it; 125 + } 126 + ); 127 + default = null; 128 + example = "/run/secrets/umamiDatabaseUrl"; 129 + description = '' 130 + A file containing a connection string for the database. The connection string 131 + must start with `postgresql://`, `postgres://` or `mysql://`. 132 + If using this, then DATABASE_TYPE must be set to the appropriate value. 133 + The contents of the file are read through systemd credentials, therefore the 134 + user running umami does not need permissions to read the file. 135 + ''; 136 + }; 137 + DATABASE_TYPE = mkOption { 138 + type = types.nullOr ( 139 + types.enum [ 140 + "postgresql" 141 + "mysql" 142 + ] 143 + ); 144 + default = 145 + if cfg.settings.DATABASE_URL != null && hasPrefix "mysql://" cfg.settings.DATABASE_URL then 146 + "mysql" 147 + else 148 + "postgresql"; 149 + defaultText = literalExpression ''if config.services.umami.settings.DATABASE_URL != null && hasPrefix "mysql://" config.services.umami.settings.DATABASE_URL then "mysql" else "postgresql"''; 150 + example = "mysql"; 151 + description = '' 152 + The type of database to use. This is automatically inferred from DATABASE_URL, but 153 + must be set manually if you are using DATABASE_URL_FILE. 154 + ''; 155 + }; 156 + COLLECT_API_ENDPOINT = mkOption { 157 + type = types.nullOr types.str; 158 + default = null; 159 + example = "/api/alternate-send"; 160 + description = '' 161 + Allows you to send metrics to a location different than the default `/api/send`. 162 + ''; 163 + }; 164 + TRACKER_SCRIPT_NAME = mkOption { 165 + type = types.listOf types.str; 166 + default = [ ]; 167 + example = [ "tracker.js" ]; 168 + description = '' 169 + Allows you to assign a custom name to the tracker script different from the default `script.js`. 170 + ''; 171 + }; 172 + BASE_PATH = mkOption { 173 + type = types.str; 174 + default = ""; 175 + example = "/analytics"; 176 + description = '' 177 + Allows you to host Umami under a subdirectory. 178 + You may need to update your reverse proxy settings to correctly handle the BASE_PATH prefix. 179 + ''; 180 + }; 181 + DISABLE_UPDATES = mkOption { 182 + type = types.bool; 183 + default = true; 184 + example = false; 185 + description = '' 186 + Disables the check for new versions of Umami. 187 + ''; 188 + }; 189 + DISABLE_TELEMETRY = mkOption { 190 + type = types.bool; 191 + default = false; 192 + example = true; 193 + description = '' 194 + Umami collects completely anonymous telemetry data in order help improve the application. 195 + You can choose to disable this if you don't want to participate. 196 + ''; 197 + }; 198 + HOSTNAME = mkOption { 199 + type = types.str; 200 + default = "127.0.0.1"; 201 + example = "0.0.0.0"; 202 + description = '' 203 + The address to listen on. 204 + ''; 205 + }; 206 + PORT = mkOption { 207 + type = types.port; 208 + default = 3000; 209 + example = 3010; 210 + description = '' 211 + The port to listen on. 212 + ''; 213 + }; 214 + }; 215 + }; 216 + 217 + default = { }; 218 + 219 + example = { 220 + APP_SECRET_FILE = "/run/secrets/umamiAppSecret"; 221 + DISABLE_TELEMETRY = true; 222 + }; 223 + }; 224 + }; 225 + 226 + config = mkIf cfg.enable { 227 + assertions = [ 228 + { 229 + assertion = (cfg.settings.APP_SECRET_FILE != null) != (cfg.settings ? APP_SECRET); 230 + message = "One (and only one) of services.umami.settings.APP_SECRET_FILE and services.umami.settings.APP_SECRET must be set."; 231 + } 232 + { 233 + assertion = (cfg.settings.DATABASE_URL_FILE != null) != (cfg.settings.DATABASE_URL != null); 234 + message = "One (and only one) of services.umami.settings.DATABASE_URL_FILE and services.umami.settings.DATABASE_URL must be set."; 235 + } 236 + { 237 + assertion = 238 + cfg.createPostgresqlDatabase 239 + -> cfg.settings.DATABASE_URL == "postgresql://umami@localhost/umami?host=/run/postgresql"; 240 + message = "The option config.services.umami.createPostgresqlDatabase is enabled, but config.services.umami.settings.DATABASE_URL has been modified."; 241 + } 242 + ]; 243 + 244 + services.postgresql = mkIf cfg.createPostgresqlDatabase { 245 + enable = true; 246 + ensureDatabases = [ "umami" ]; 247 + ensureUsers = [ 248 + { 249 + name = "umami"; 250 + ensureDBOwnership = true; 251 + ensureClauses.login = true; 252 + } 253 + ]; 254 + }; 255 + 256 + systemd.services.umami = { 257 + environment = mapAttrs (_: toString) nonFileSettings; 258 + 259 + description = "Umami: a simple, fast, privacy-focused alternative to Google Analytics"; 260 + after = [ "network.target" ] ++ (optional (cfg.createPostgresqlDatabase) "postgresql.service"); 261 + wantedBy = [ "multi-user.target" ]; 262 + 263 + script = 264 + let 265 + loadCredentials = 266 + (optional ( 267 + cfg.settings.APP_SECRET_FILE != null 268 + ) ''export APP_SECRET="$(systemd-creds cat appSecret)"'') 269 + ++ (optional ( 270 + cfg.settings.DATABASE_URL_FILE != null 271 + ) ''export DATABASE_URL="$(systemd-creds cat databaseUrl)"''); 272 + in 273 + '' 274 + ${concatStringsSep "\n" loadCredentials} 275 + ${getExe cfg.package} 276 + ''; 277 + 278 + serviceConfig = { 279 + Type = "simple"; 280 + Restart = "on-failure"; 281 + RestartSec = 3; 282 + DynamicUser = true; 283 + 284 + LoadCredential = 285 + (optional (cfg.settings.APP_SECRET_FILE != null) "appSecret:${cfg.settings.APP_SECRET_FILE}") 286 + ++ (optional ( 287 + cfg.settings.DATABASE_URL_FILE != null 288 + ) "databaseUrl:${cfg.settings.DATABASE_URL_FILE}"); 289 + 290 + # Hardening 291 + CapabilityBoundingSet = ""; 292 + NoNewPrivileges = true; 293 + PrivateUsers = true; 294 + PrivateTmp = true; 295 + PrivateDevices = true; 296 + PrivateMounts = true; 297 + ProtectClock = true; 298 + ProtectControlGroups = true; 299 + ProtectHome = true; 300 + ProtectHostname = true; 301 + ProtectKernelLogs = true; 302 + ProtectKernelModules = true; 303 + ProtectKernelTunables = true; 304 + RestrictAddressFamilies = (optional cfg.createPostgresqlDatabase "AF_UNIX") ++ [ 305 + "AF_INET" 306 + "AF_INET6" 307 + ]; 308 + RestrictNamespaces = true; 309 + RestrictRealtime = true; 310 + RestrictSUIDSGID = true; 311 + }; 312 + }; 313 + }; 314 + 315 + meta.maintainers = with maintainers; [ diogotcorreia ]; 316 + }
+1
nixos/tests/all-tests.nix
··· 1509 1509 ucarp = runTest ./ucarp.nix; 1510 1510 udisks2 = runTest ./udisks2.nix; 1511 1511 ulogd = runTest ./ulogd/ulogd.nix; 1512 + umami = runTest ./web-apps/umami.nix; 1512 1513 umurmur = runTest ./umurmur.nix; 1513 1514 unbound = runTest ./unbound.nix; 1514 1515 unifi = runTest ./unifi.nix;
+45
nixos/tests/web-apps/umami.nix
··· 1 + { lib, ... }: 2 + { 3 + name = "umami-nixos"; 4 + 5 + meta.maintainers = with lib.maintainers; [ diogotcorreia ]; 6 + 7 + nodes.machine = 8 + { pkgs, ... }: 9 + { 10 + services.umami = { 11 + enable = true; 12 + settings = { 13 + APP_SECRET = "very_secret"; 14 + }; 15 + }; 16 + }; 17 + 18 + testScript = '' 19 + import json 20 + 21 + machine.wait_for_unit("umami.service") 22 + 23 + machine.wait_for_open_port(3000) 24 + machine.succeed("curl --fail http://localhost:3000/") 25 + machine.succeed("curl --fail http://localhost:3000/script.js") 26 + 27 + res = machine.succeed(""" 28 + curl -f --json '{ "username": "admin", "password": "umami" }' http://localhost:3000/api/auth/login 29 + """) 30 + token = json.loads(res)['token'] 31 + 32 + res = machine.succeed(""" 33 + curl -f -H 'Authorization: Bearer %s' --json '{ "domain": "localhost", "name": "Test" }' http://localhost:3000/api/websites 34 + """ % token) 35 + print(res) 36 + websiteId = json.loads(res)['id'] 37 + 38 + res = machine.succeed(""" 39 + curl -f -H 'Authorization: Bearer %s' http://localhost:3000/api/websites/%s 40 + """ % (token, websiteId)) 41 + website = json.loads(res) 42 + assert website["name"] == "Test" 43 + assert website["domain"] == "localhost" 44 + ''; 45 + }
+201
pkgs/by-name/um/umami/package.nix
··· 1 + { 2 + lib, 3 + stdenvNoCC, 4 + fetchFromGitHub, 5 + fetchurl, 6 + makeWrapper, 7 + nixosTests, 8 + nodejs, 9 + pnpm_10, 10 + prisma-engines, 11 + openssl, 12 + rustPlatform, 13 + # build variables 14 + databaseType ? "postgresql", 15 + collectApiEndpoint ? "", 16 + trackerScriptNames ? [ ], 17 + basePath ? "", 18 + }: 19 + let 20 + sources = lib.importJSON ./sources.json; 21 + pnpm = pnpm_10; 22 + 23 + geocities = stdenvNoCC.mkDerivation { 24 + pname = "umami-geocities"; 25 + version = sources.geocities.date; 26 + src = fetchurl { 27 + url = "https://raw.githubusercontent.com/GitSquared/node-geolite2-redist/${sources.geocities.rev}/redist/GeoLite2-City.tar.gz"; 28 + inherit (sources.geocities) hash; 29 + }; 30 + 31 + doBuild = false; 32 + 33 + installPhase = '' 34 + mkdir -p $out 35 + cp ./GeoLite2-City.mmdb $out/GeoLite2-City.mmdb 36 + ''; 37 + 38 + meta.license = lib.licenses.cc-by-40; 39 + }; 40 + 41 + # Pin the specific version of prisma to the one used by upstream 42 + # to guarantee compatibility. 43 + prisma-engines' = prisma-engines.overrideAttrs (old: rec { 44 + version = "6.7.0"; 45 + src = fetchFromGitHub { 46 + owner = "prisma"; 47 + repo = "prisma-engines"; 48 + tag = version; 49 + hash = "sha256-Ty8BqWjZluU6a5xhSAVb2VoTVY91UUj6zoVXMKeLO4o="; 50 + }; 51 + cargoHash = "sha256-HjDoWa/JE6izUd+hmWVI1Yy3cTBlMcvD9ANsvqAoHBI="; 52 + 53 + cargoDeps = rustPlatform.fetchCargoVendor { 54 + inherit (old) pname; 55 + inherit src version; 56 + hash = cargoHash; 57 + }; 58 + }); 59 + in 60 + stdenvNoCC.mkDerivation (finalAttrs: { 61 + pname = "umami"; 62 + version = "2.19.0"; 63 + 64 + nativeBuildInputs = [ 65 + makeWrapper 66 + nodejs 67 + pnpm.configHook 68 + ]; 69 + 70 + src = fetchFromGitHub { 71 + owner = "umami-software"; 72 + repo = "umami"; 73 + tag = "v${finalAttrs.version}"; 74 + hash = "sha256-luiwGmCujbFGWANSCOiHIov56gsMQ6M+Bj0stcz9he8="; 75 + }; 76 + 77 + # install dev dependencies as well, for rollup 78 + pnpmInstallFlags = [ "--prod=false" ]; 79 + 80 + pnpmDeps = pnpm.fetchDeps { 81 + inherit (finalAttrs) 82 + pname 83 + pnpmInstallFlags 84 + version 85 + src 86 + ; 87 + fetcherVersion = 2; 88 + hash = "sha256-2GiCeCt/mU5Dm5YHQgJF3127WPHq5QLX8JRcUv6B6lE="; 89 + }; 90 + 91 + env.CYPRESS_INSTALL_BINARY = "0"; 92 + env.NODE_ENV = "production"; 93 + env.NEXT_TELEMETRY_DISABLED = "1"; 94 + 95 + # copy-db-files uses this variable to decide which Prisma schema to use 96 + env.DATABASE_TYPE = databaseType; 97 + 98 + env.COLLECT_API_ENDPOINT = collectApiEndpoint; 99 + env.TRACKER_SCRIPT_NAME = lib.concatStringsSep "," trackerScriptNames; 100 + env.BASE_PATH = basePath; 101 + 102 + # Allow prisma-cli to find prisma-engines without having to download them 103 + env.PRISMA_QUERY_ENGINE_LIBRARY = "${prisma-engines'}/lib/libquery_engine.node"; 104 + env.PRISMA_SCHEMA_ENGINE_BINARY = "${prisma-engines'}/bin/schema-engine"; 105 + 106 + buildPhase = '' 107 + runHook preBuild 108 + 109 + pnpm copy-db-files 110 + pnpm build-db-client # prisma generate 111 + 112 + pnpm build-tracker 113 + pnpm build-app 114 + 115 + runHook postBuild 116 + ''; 117 + 118 + checkPhase = '' 119 + runHook preCheck 120 + 121 + pnpm test 122 + 123 + runHook postCheck 124 + ''; 125 + 126 + doCheck = true; 127 + 128 + installPhase = '' 129 + runHook preInstall 130 + 131 + mv .next/standalone $out 132 + mv .next/static $out/.next/static 133 + 134 + # Include prisma cli in next standalone build. 135 + # This is preferred to using the prisma in nixpkgs because it guarantees 136 + # the version matches. 137 + # See https://nextjs-forum.com/post/1280550687998083198 138 + # and https://nextjs.org/docs/pages/api-reference/config/next-config-js/output#caveats 139 + # Unfortunately, using outputFileTracingIncludes doesn't work because of pnpm's symlink structure, 140 + # so we just copy the files manually. 141 + mkdir -p $out/node_modules/.bin 142 + cp node_modules/.bin/prisma $out/node_modules/.bin 143 + cp -a node_modules/prisma $out/node_modules 144 + cp -a node_modules/.pnpm/@prisma* $out/node_modules/.pnpm 145 + cp -a node_modules/.pnpm/prisma* $out/node_modules/.pnpm 146 + # remove broken symlinks (some dependencies that are not relevant for running migrations) 147 + find "$out"/node_modules/.pnpm/@prisma* -xtype l -exec rm {} \; 148 + find "$out"/node_modules/.pnpm/prisma* -xtype l -exec rm {} \; 149 + 150 + cp -R public $out/public 151 + cp -R prisma $out/prisma 152 + 153 + ln -s ${geocities} $out/geo 154 + 155 + mkdir -p $out/bin 156 + # Run database migrations before starting umami. 157 + # Add openssl to PATH since it is required for prisma to make SSL connections. 158 + # Force working directory to $out because umami assumes many paths are relative to it (e.g., prisma and geolite). 159 + makeWrapper ${nodejs}/bin/node $out/bin/umami-server \ 160 + --set NODE_ENV production \ 161 + --set NEXT_TELEMETRY_DISABLED 1 \ 162 + --set PRISMA_QUERY_ENGINE_LIBRARY "${prisma-engines'}/lib/libquery_engine.node" \ 163 + --set PRISMA_SCHEMA_ENGINE_BINARY "${prisma-engines'}/bin/schema-engine" \ 164 + --prefix PATH : ${ 165 + lib.makeBinPath [ 166 + openssl 167 + nodejs 168 + ] 169 + } \ 170 + --chdir $out \ 171 + --run "$out/node_modules/.bin/prisma migrate deploy" \ 172 + --add-flags "$out/server.js" 173 + 174 + runHook postInstall 175 + ''; 176 + 177 + passthru = { 178 + tests = { 179 + inherit (nixosTests) umami; 180 + }; 181 + inherit 182 + sources 183 + geocities 184 + ; 185 + prisma-engines = prisma-engines'; 186 + updateScript = ./update.sh; 187 + }; 188 + 189 + meta = with lib; { 190 + changelog = "https://github.com/umami-software/umami/releases/tag/v${finalAttrs.version}"; 191 + description = "Simple, easy to use, self-hosted web analytics solution"; 192 + homepage = "https://umami.is/"; 193 + license = with lib.licenses; [ 194 + mit 195 + cc-by-40 # geocities 196 + ]; 197 + platforms = lib.platforms.linux; 198 + mainProgram = "umami-server"; 199 + maintainers = with maintainers; [ diogotcorreia ]; 200 + }; 201 + })
+7
pkgs/by-name/um/umami/sources.json
··· 1 + { 2 + "geocities": { 3 + "rev": "0817bc800279e26e9ff045b7b129385e5b23012e", 4 + "date": "2025-07-29", 5 + "hash": "sha256-Rw9UEvUu7rtXFvHEqKza6kn9LwT6C17zJ/ljoN+t6Ek=" 6 + } 7 + }
+50
pkgs/by-name/um/umami/update.sh
··· 1 + #!/usr/bin/env nix-shell 2 + #!nix-shell -i bash -p curl jq prefetch-yarn-deps nix-prefetch-github coreutils nix-update 3 + # shellcheck shell=bash 4 + 5 + # This script exists to update geocities version and pin prisma-engines version 6 + 7 + set -euo pipefail 8 + SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" 9 + 10 + old_version=$(nix-instantiate --eval -A 'umami.version' default.nix | tr -d '"' || echo "0.0.1") 11 + version=$(curl -s "https://api.github.com/repos/umami-software/umami/releases/latest" | jq -r ".tag_name") 12 + version="${version#v}" 13 + 14 + echo "Updating to $version" 15 + 16 + if [[ "$old_version" == "$version" ]]; then 17 + echo "Already up to date!" 18 + exit 0 19 + fi 20 + 21 + nix-update --version "$version" umami 22 + 23 + echo "Fetching geolite" 24 + geocities_rev_date=$(curl https://api.github.com/repos/GitSquared/node-geolite2-redist/branches/master | jq -r ".commit.sha, .commit.commit.author.date") 25 + geocities_rev=$(echo "$geocities_rev_date" | head -1) 26 + geocities_date=$(echo "$geocities_rev_date" | tail -1 | sed 's/T.*//') 27 + 28 + # upstream is kind enough to provide a file with the hash of the tar.gz archive 29 + geocities_hash=$(curl -s "https://raw.githubusercontent.com/GitSquared/node-geolite2-redist/$geocities_rev/redist/GeoLite2-City.tar.gz.sha256") 30 + geocities_hash_sri=$(nix-hash --to-sri --type sha256 "$geocities_hash") 31 + 32 + cat <<EOF > "$SCRIPT_DIR/sources.json" 33 + { 34 + "geocities": { 35 + "rev": "$geocities_rev", 36 + "date": "$geocities_date", 37 + "hash": "$geocities_hash_sri" 38 + } 39 + } 40 + EOF 41 + 42 + echo "Pinning Prisma version" 43 + upstream_src="https://raw.githubusercontent.com/umami-software/umami/v$version" 44 + 45 + lock=$(mktemp) 46 + curl -s -o "$lock" "$upstream_src/pnpm-lock.yaml" 47 + prisma_version=$(grep "@prisma/engines@" "$lock" | head -n1 | awk -F"[@']" '{print $4}') 48 + rm "$lock" 49 + 50 + nix-update --version "$prisma_version" umami.prisma-engines