change(nix): dedupe nix lockfile checking scripts in ci (#18000)

* change(nix): dedupe nix lockfile checking scripts in ci

* feat(nix): make .#fix-lockfiles run --apply if no args passed

* fix(nix): use same nodejs version everywhere & small lints

- prevent lockfile thrashing while using nix :3
- use lib.getExe instead of raw /bin/ paths
- use inputs'.self instead of passing system in manually

* fix(nix): update lock files yet again (hopefully for the last time)

* fix(nix): align indentation of collision check echo

---------

Co-authored-by: Hermes Agent <hermes@nousresearch.com>
This commit is contained in:
ethernet
2026-04-30 13:22:30 -04:00
committed by GitHub
parent 4e296dcdda
commit 2d3c041338
10 changed files with 203 additions and 171 deletions

View File

@@ -4,9 +4,9 @@
# transitive deps like onnxruntime that lack compatible wheels on
# aarch64-darwin. The package and devShell still work on macOS.
{ inputs, ... }: {
perSystem = { pkgs, system, lib, ... }:
perSystem = { pkgs, lib, self', ... }:
let
hermes-agent = inputs.self.packages.${system}.default;
hermes-agent = self'.packages.default;
hermesVenv = hermes-agent.hermesVenv;
configMergeScript = pkgs.callPackage ./configMergeScript.nix { };
@@ -51,7 +51,7 @@ json.dump(sorted(leaf_paths(DEFAULT_CONFIG)), sys.stdout, indent=2)
failMsg = lib.concatMapStringsSep "\n" (r: " - ${r.sys}") failures;
in pkgs.runCommand "hermes-cross-eval" { } (
if failures != [] then
builtins.throw "Package fails to evaluate on:\n${failMsg}"
throw "Package fails to evaluate on:\n${failMsg}"
else ''
echo "PASS: package evaluates on all ${toString (builtins.length targetSystems)} platforms"
mkdir -p $out

View File

@@ -1,29 +1,30 @@
# nix/devShell.nix — Dev shell that delegates setup to each package
#
# Each package in inputsFrom exposes passthru.devShellHook — a bash snippet
# Each package in inputsFrom might expose passthru.devShellHook — a bash snippet
# with stamp-checked setup logic. This file collects and runs them all.
{ inputs, ... }: {
perSystem = { pkgs, system, ... }:
{ ... }:
{
perSystem =
{ pkgs, self', ... }:
let
hermes-agent = inputs.self.packages.${system}.default;
hermes-tui = inputs.self.packages.${system}.tui;
hermes-web = inputs.self.packages.${system}.web;
packages = [ hermes-agent hermes-tui hermes-web ];
in {
packages = builtins.attrValues self'.packages;
in
{
devShells.default = pkgs.mkShell {
inputsFrom = packages;
packages = with pkgs; [
python312 uv nodejs_22 ripgrep git openssh ffmpeg
uv
];
shellHook = let
hooks = map (p: p.passthru.devShellHook or "") packages;
combined = pkgs.lib.concatStringsSep "\n" (builtins.filter (h: h != "") hooks);
in ''
echo "Hermes Agent dev shell"
${combined}
echo "Ready. Run 'hermes' to start."
'';
shellHook =
let
hooks = map (p: p.passthru.devShellHook or "") packages;
combined = pkgs.lib.concatStringsSep "\n" (builtins.filter (h: h != "") hooks);
in
''
echo "Hermes Agent dev shell"
${combined}
echo "Ready. Run 'hermes' to start."
'';
};
};
}

View File

@@ -27,12 +27,13 @@
extraPythonPackages ? [ ],
}:
let
nodejs = nodejs_22;
hermesVenv = callPackage ./python.nix {
inherit uv2nix pyproject-nix pyproject-build-systems;
};
hermesNpmLib = callPackage ./lib.nix {
inherit npm-lockfile-fix;
inherit npm-lockfile-fix nodejs;
};
hermesTui = callPackage ./tui.nix {
@@ -57,7 +58,7 @@ let
};
runtimeDeps = [
nodejs_22
nodejs
ripgrep
git
openssh
@@ -82,10 +83,49 @@ let
builtins.hashString "sha256" (builtins.readFile ../uv.lock)
else
"none";
checkPackageCollisions = ''
import pathlib, sys, re
def canonical(name):
return re.sub(r'[-_.]+', '-', name).lower()
# Collect core venv package names
core = set()
venv_sp = pathlib.Path('${hermesVenv}/${sitePackagesPath}')
for di in venv_sp.glob('*.dist-info'):
meta = di / 'METADATA'
if meta.exists():
for line in meta.read_text().splitlines():
if line.startswith('Name:'):
core.add(canonical(line.split(':', 1)[1].strip()))
break
# Check each extra package for collisions
extras_dirs = [${lib.concatMapStringsSep ", " (p: "'${toString p}'") allExtraPythonPackages}]
for edir in extras_dirs:
sp = pathlib.Path(edir) / '${sitePackagesPath}'
if not sp.exists():
continue
for di in sp.glob('*.dist-info'):
meta = di / 'METADATA'
if not meta.exists():
continue
for line in meta.read_text().splitlines():
if line.startswith('Name:'):
pkg = canonical(line.split(':', 1)[1].strip())
if pkg in core:
print(f'ERROR: plugin package \"{pkg}\" collides with a package in hermes sealed venv', file=sys.stderr)
print(f' from: {di}', file=sys.stderr)
print(f' Remove this dependency from extraPythonPackages.', file=sys.stderr)
sys.exit(1)
break
print('No collisions found.')
'';
in
stdenv.mkDerivation {
pname = "hermes-agent";
version = (builtins.fromTOML (builtins.readFile ../pyproject.toml)).project.version;
version = (fromTOML (builtins.readFile ../pyproject.toml)).project.version;
dontUnpack = true;
dontBuild = true;
@@ -111,7 +151,7 @@ stdenv.mkDerivation {
--set HERMES_WEB_DIST $out/share/hermes-agent/web_dist \
--set HERMES_TUI_DIR $out/ui-tui \
--set HERMES_PYTHON ${hermesVenv}/bin/python3 \
--set HERMES_NODE ${nodejs_22}/bin/node \
--set HERMES_NODE ${lib.getExe nodejs} \
${lib.optionalString (rev != null) ''--set HERMES_REVISION ${rev} \''}
${lib.optionalString (extraPythonPackages != [ ]) ''--suffix PYTHONPATH : "${pythonPath}"''}
'')
@@ -124,45 +164,7 @@ stdenv.mkDerivation {
${lib.optionalString (extraPythonPackages != [ ]) ''
echo "=== Checking for plugin/core package collisions ==="
${hermesVenv}/bin/python3 -c "
import pathlib, sys, re
def canonical(name):
return re.sub(r'[-_.]+', '-', name).lower()
# Collect core venv package names
core = set()
venv_sp = pathlib.Path('${hermesVenv}/${sitePackagesPath}')
for di in venv_sp.glob('*.dist-info'):
meta = di / 'METADATA'
if meta.exists():
for line in meta.read_text().splitlines():
if line.startswith('Name:'):
core.add(canonical(line.split(':', 1)[1].strip()))
break
# Check each extra package for collisions
extras_dirs = [${lib.concatMapStringsSep ", " (p: "'${toString p}'") allExtraPythonPackages}]
for edir in extras_dirs:
sp = pathlib.Path(edir) / '${sitePackagesPath}'
if not sp.exists():
continue
for di in sp.glob('*.dist-info'):
meta = di / 'METADATA'
if not meta.exists():
continue
for line in meta.read_text().splitlines():
if line.startswith('Name:'):
pkg = canonical(line.split(':', 1)[1].strip())
if pkg in core:
print(f'ERROR: plugin package \"{pkg}\" collides with a package in hermes sealed venv', file=sys.stderr)
print(f' from: {di}', file=sys.stderr)
print(f' Remove this dependency from extraPythonPackages.', file=sys.stderr)
sys.exit(1)
break
print('No collisions found.')
"
${hermesVenv}/bin/python3 -c "${checkPackageCollisions}"
echo "=== No collisions ==="
''}
@@ -170,7 +172,12 @@ print('No collisions found.')
'';
passthru = {
inherit hermesTui hermesWeb hermesNpmLib hermesVenv;
inherit
hermesTui
hermesWeb
hermesNpmLib
hermesVenv
;
devShellHook = ''
STAMP=".nix-stamps/hermes-agent"

View File

@@ -1,11 +1,16 @@
# nix/lib.nix — Shared helpers for nix stuff
{ pkgs, npm-lockfile-fix }:
{
pkgs,
npm-lockfile-fix,
nodejs,
}:
{
# Returns a buildNpmPackage-compatible attrs set that provides:
# patchPhase — ensures lockfile has exactly one trailing newline
# nativeBuildInputs — [ updateLockfileScript ] (list, prepend with ++ for more)
# patchPhase — ensures lockfile has exactly one trailing newline
# nativeBuildInputs — [ updateLockfileScript ] (list, prepend with ++ for more)
# passthru.devShellHook — stamp-checked npm install + hash auto-update
# passthru.npmLockfile — metadata for mkFixLockfiles
# nodejs — fixed nodejs version for all packages we use in the repo
#
# NOTE: npmConfigHook runs `diff` between the source lockfile and the
# npm-deps cache lockfile. fetchNpmDeps preserves whatever trailing
@@ -24,6 +29,7 @@
nixFile ? "nix/${attr}.nix", # defaults to nix/<attr>.nix
}:
{
inherit nodejs;
patchPhase = ''
runHook prePatch
# Normalize trailing newlines so source and npm-deps always match,
@@ -56,8 +62,8 @@
cd "$REPO_ROOT/${folder}"
rm -rf node_modules/
npm cache clean --force
CI=true npm install
${pkgs.lib.getExe' nodejs "npm"} cache clean --force
CI=true ${pkgs.lib.getExe' nodejs "npm"} install
${pkgs.lib.getExe npm-lockfile-fix} ./package-lock.json
NIX_FILE="$REPO_ROOT/${nixFile}"
@@ -83,7 +89,7 @@
STAMP_VALUE="$(_hermes_npm_stamp)"
if [ ! -f "$STAMP" ] || [ "$(cat "$STAMP")" != "$STAMP_VALUE" ]; then
echo "${pname}: installing npm dependencies..."
( cd ${folder} && CI=true npm install --silent --no-fund --no-audit 2>/dev/null )
( cd ${folder} && CI=true ${pkgs.lib.getExe' nodejs "npm"} install --silent --no-fund --no-audit 2>/dev/null )
# Auto-update the nix hash so it stays in sync with the lockfile
echo "${pname}: prefetching npm deps..."
@@ -92,7 +98,7 @@
sed -i "s|hash = \"sha256-[A-Za-z0-9+/=]+\"|hash = \"$NEW_HASH\";|" "$NIX_FILE"
echo "${pname}: updated hash to $NEW_HASH"
else
echo "${pname}: warning: prefetch failed, run 'nix run .#fix-lockfiles -- --apply' manually" >&2
echo "${pname}: warning: prefetch failed, run 'nix run .#fix-lockfiles' manually" >&2
fi
mkdir -p .nix-stamps
@@ -112,6 +118,7 @@
# Invocations:
# fix-lockfiles --check # exit 1 if any hash is stale
# fix-lockfiles --apply # rewrite stale hashes in place
# fix-lockfiles # alias of --apply
# Writes machine-readable fields (stale, changed, report) to $GITHUB_OUTPUT
# when set, so CI workflows can post a sticky PR comment directly.
mkFixLockfiles =
@@ -124,7 +131,7 @@
in
pkgs.writeShellScriptBin "fix-lockfiles" ''
set -uox pipefail
MODE="''${1:---check}"
MODE="''${1:---apply}"
case "$MODE" in
--check|--apply) ;;
-h|--help)
@@ -222,7 +229,7 @@
if [ "$STALE" -eq 1 ] && [ "$MODE" = "--check" ]; then
echo
echo "Stale lockfile hashes detected. Run:"
echo " nix run .#fix-lockfiles -- --apply"
echo " nix run .#fix-lockfiles"
exit 1
fi

View File

@@ -4,7 +4,7 @@ let
src = ../ui-tui;
npmDeps = pkgs.fetchNpmDeps {
inherit src;
hash = "sha256-a/HGI9OgVcTnZrMXA7xFMGnFoVxyHe95fulVz+WNYB0=";
hash = "sha256-Chz+NW9NXqboXHOa6PKwf5bhAkkcFtKNhvKWwg2XSPc=";
};
npm = hermesNpmLib.mkNpmPassthru { folder = "ui-tui"; attr = "tui"; pname = "hermes-tui"; };

View File

@@ -8,11 +8,13 @@ let
};
npm = hermesNpmLib.mkNpmPassthru { folder = "web"; attr = "web"; pname = "hermes-web"; };
packageJson = builtins.fromJSON (builtins.readFile (src + "/package.json"));
version = packageJson.version;
in
pkgs.buildNpmPackage (npm // {
pname = "hermes-web";
version = "0.0.0";
inherit src npmDeps;
inherit src npmDeps version;
doCheck = false;