diff --git a/.github/workflows/nix-lockfile-check.yml b/.github/workflows/nix-lockfile-check.yml deleted file mode 100644 index 3e69feb1f..000000000 --- a/.github/workflows/nix-lockfile-check.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Nix Lockfile Check - -on: - pull_request: - paths: - - 'ui-tui/package.json' - - 'ui-tui/package-lock.json' - - 'web/package.json' - - 'web/package-lock.json' - - 'nix/tui.nix' - - 'nix/web.nix' - - 'nix/lib.nix' - workflow_dispatch: - -permissions: - contents: read - pull-requests: write - -concurrency: - group: nix-lockfile-check-${{ github.ref }} - cancel-in-progress: true - -jobs: - check: - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - - uses: nixbuild/nix-quick-install-action@63ca48f939ee3b8d835f4126562537df0fee5b91 # v30 - - - name: Check lockfile hashes - id: check - continue-on-error: true - run: nix run .#fix-lockfiles -- --check - - - name: Post sticky PR comment (stale) - if: steps.check.outputs.stale == 'true' && github.event_name == 'pull_request' - uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1 - with: - header: nix-lockfile-check - message: | - ### ⚠️ npm lockfile hash out of date - - The `hash = "sha256-..."` line in these nix files no longer matches the committed `package-lock.json`: - - ${{ steps.check.outputs.report }} - - #### Apply the fix - - - [ ] **Apply lockfile fix** — tick to push a commit with the correct hashes to this PR branch - - Or [run the Nix Lockfile Fix workflow](${{ github.server_url }}/${{ github.repository }}/actions/workflows/nix-lockfile-fix.yml) manually (pass PR `#${{ github.event.pull_request.number }}`) - - Or locally: `nix run .#fix-lockfiles -- --apply` and commit the diff - - - name: Clear sticky PR comment (resolved) - if: steps.check.outputs.stale == 'false' && github.event_name == 'pull_request' - uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1 - with: - header: nix-lockfile-check - delete: true - - - name: Fail if stale - if: steps.check.outputs.stale == 'true' - run: exit 1 diff --git a/.github/workflows/nix-lockfile-fix.yml b/.github/workflows/nix-lockfile-fix.yml deleted file mode 100644 index a64d45510..000000000 --- a/.github/workflows/nix-lockfile-fix.yml +++ /dev/null @@ -1,126 +0,0 @@ -name: Nix Lockfile Fix - -on: - workflow_dispatch: - inputs: - pr_number: - description: 'PR number to fix (leave empty to run on the selected branch)' - required: false - type: string - issue_comment: - types: [edited] - -permissions: - contents: write - pull-requests: write - -concurrency: - group: nix-lockfile-fix-${{ github.event.issue.number || github.event.inputs.pr_number || github.ref }} - cancel-in-progress: false - -jobs: - fix: - # Run on manual dispatch OR when a task-list checkbox in the sticky - # lockfile-check comment flips from `[ ]` to `[x]`. - if: | - github.event_name == 'workflow_dispatch' || - (github.event_name == 'issue_comment' - && github.event.issue.pull_request != null - && contains(github.event.comment.body, '[x] **Apply lockfile fix**') - && !contains(github.event.changes.body.from, '[x] **Apply lockfile fix**')) - runs-on: ubuntu-latest - timeout-minutes: 25 - steps: - - name: Authorize & resolve PR - id: resolve - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - script: | - // 1. Verify the actor has write access — applies to both checkbox - // clicks and manual dispatch. - const { data: perm } = - await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: context.actor, - }); - if (!['admin', 'write', 'maintain'].includes(perm.permission)) { - core.setFailed( - `${context.actor} lacks write access (has: ${perm.permission})` - ); - return; - } - - // 2. Resolve which ref to check out. - let prNumber = ''; - if (context.eventName === 'issue_comment') { - prNumber = String(context.payload.issue.number); - } else if (context.eventName === 'workflow_dispatch') { - prNumber = context.payload.inputs.pr_number || ''; - } - - if (!prNumber) { - core.setOutput('ref', context.ref.replace(/^refs\/heads\//, '')); - core.setOutput('repo', context.repo.repo); - core.setOutput('owner', context.repo.owner); - core.setOutput('pr', ''); - return; - } - - const { data: pr } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: Number(prNumber), - }); - core.setOutput('ref', pr.head.ref); - core.setOutput('repo', pr.head.repo.name); - core.setOutput('owner', pr.head.repo.owner.login); - core.setOutput('pr', String(pr.number)); - - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - repository: ${{ steps.resolve.outputs.owner }}/${{ steps.resolve.outputs.repo }} - ref: ${{ steps.resolve.outputs.ref }} - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 - - - uses: nixbuild/nix-quick-install-action@63ca48f939ee3b8d835f4126562537df0fee5b91 # v30 - - - name: Apply lockfile hashes - id: apply - run: nix run .#fix-lockfiles -- --apply - - - name: Commit & push - if: steps.apply.outputs.changed == 'true' - shell: bash - run: | - set -euo pipefail - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add nix/tui.nix nix/web.nix - git commit -m "fix(nix): refresh npm lockfile hashes" - git push - - - name: Comment on PR (applied) - if: steps.apply.outputs.changed == 'true' && steps.resolve.outputs.pr != '' - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - script: | - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: Number('${{ steps.resolve.outputs.pr }}'), - body: 'Pushed a commit refreshing the npm lockfile hashes.', - }); - - - name: Comment on PR (already current) - if: steps.apply.outputs.changed == 'false' && steps.resolve.outputs.pr != '' - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - script: | - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: Number('${{ steps.resolve.outputs.pr }}'), - body: 'npm lockfile hashes are already current — nothing to commit.', -z }); diff --git a/nix/devShell.nix b/nix/devShell.nix index d0d56e40b..63edc59cf 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -7,8 +7,7 @@ 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 ]; + packages = [ hermes-agent hermes-tui ]; in { devShells.default = pkgs.mkShell { inputsFrom = packages; diff --git a/nix/lib.nix b/nix/lib.nix deleted file mode 100644 index f2e1c8291..000000000 --- a/nix/lib.nix +++ /dev/null @@ -1,151 +0,0 @@ -# nix/lib.nix — Shared helpers for nix stuff -{ pkgs, npm-lockfile-fix }: -{ - # Shell script that refreshes node_modules, fixes the lockfile, and - # rewrites the `hash = "sha256-..."` line in the given nix file so - # fetchNpmDeps picks up the new package-lock.json. - mkUpdateLockfileScript = - { - name, # script binary name, e.g. "update_tui_lockfile" - folder, # repo-relative folder with package.json, e.g. "ui-tui" - nixFile, # repo-relative nix file with the hash line, e.g. "nix/tui.nix" - attr, # flake package attr to build to cause the failure, e.g. "tui" - }: - pkgs.writeShellScriptBin name '' - set -euox pipefail - - REPO_ROOT=$(git rev-parse --show-toplevel) - - cd "$REPO_ROOT/${folder}" - rm -rf node_modules/ - npm cache clean --force - CI=true npm install - ${pkgs.lib.getExe npm-lockfile-fix} ./package-lock.json - - NIX_FILE="$REPO_ROOT/${nixFile}" - sed -i "s/hash = \"[^\"]*\";/hash = \"\";/" $NIX_FILE - NIX_OUTPUT=$(nix build .#${attr} 2>&1 || true) - NEW_HASH=$(echo "$NIX_OUTPUT" | grep 'got:' | awk '{print $2}') - echo got new hash $NEW_HASH - sed -i "s|hash = \"[^\"]*\";|hash = \"$NEW_HASH\";|" $NIX_FILE - nix build .#${attr} - echo "Updated npm hash in $NIX_FILE to $NEW_HASH" - ''; - - # devShell bootstrap snippet: runs `npm install` in the target folder when - # package.json or package-lock.json has changed since the last install. - # Hashing happens in bash (not nix eval), and the post-install stamp is - # recomputed so a lockfile that npm rewrites during install still matches. - mkNpmDevShellHook = - { - name, # project-unique stampfile name, e.g. "hermes-tui" - folder, # repo-relative folder with package.json + package-lock.json - }: - '' - _hermes_npm_stamp() { - sha256sum "${folder}/package.json" "${folder}/package-lock.json" \ - 2>/dev/null | sha256sum | awk '{print $1}' - } - STAMP=".nix-stamps/${name}" - STAMP_VALUE="$(_hermes_npm_stamp)" - if [ ! -f "$STAMP" ] || [ "$(cat "$STAMP")" != "$STAMP_VALUE" ]; then - echo "${name}: installing npm dependencies..." - ( cd ${folder} && CI=true npm install --silent --no-fund --no-audit 2>/dev/null ) - mkdir -p .nix-stamps - _hermes_npm_stamp > "$STAMP" - fi - unset -f _hermes_npm_stamp - ''; - - # Aggregate `fix-lockfiles` bin from a list of packages carrying - # passthru.npmLockfile = { attr; folder; nixFile; }; - # Invocations: - # fix-lockfiles --check # exit 1 if any hash is stale - # fix-lockfiles --apply # rewrite stale hashes in place - # Writes machine-readable fields (stale, changed, report) to $GITHUB_OUTPUT - # when set, so CI workflows can post a sticky PR comment directly. - mkFixLockfiles = - { - packages, # list of packages with passthru.npmLockfile - }: - let - entries = map (p: p.passthru.npmLockfile) packages; - entryArgs = pkgs.lib.concatMapStringsSep " " ( - e: "\"${e.attr}:${e.folder}:${e.nixFile}\"" - ) entries; - in - pkgs.writeShellScriptBin "fix-lockfiles" '' - set -uo pipefail - MODE="''${1:---check}" - case "$MODE" in - --check|--apply) ;; - -h|--help) - echo "usage: fix-lockfiles [--check|--apply]" - exit 0 ;; - *) - echo "usage: fix-lockfiles [--check|--apply]" >&2 - exit 2 ;; - esac - - ENTRIES=(${entryArgs}) - - REPO_ROOT="$(git rev-parse --show-toplevel)" - cd "$REPO_ROOT" - - STALE=0 - FIXED=0 - REPORT="" - - for entry in "''${ENTRIES[@]}"; do - IFS=":" read -r ATTR FOLDER NIX_FILE <<< "$entry" - echo "==> .#$ATTR ($FOLDER -> $NIX_FILE)" - OUTPUT=$(nix build ".#$ATTR.npmDeps" --no-link --print-build-logs 2>&1) - STATUS=$? - if [ "$STATUS" -eq 0 ]; then - echo " ok" - continue - fi - - NEW_HASH=$(echo "$OUTPUT" | awk '/got:/ {print $2; exit}') - if [ -z "$NEW_HASH" ]; then - echo " build failed with no hash mismatch:" >&2 - echo "$OUTPUT" | tail -40 >&2 - exit 1 - fi - - OLD_HASH=$(grep -oE 'hash = "sha256-[^"]+"' "$NIX_FILE" | head -1 \ - | sed -E 's/hash = "(.*)"/\1/') - echo " stale: $OLD_HASH -> $NEW_HASH" - STALE=1 - REPORT+="- \`$NIX_FILE\` (\`.#$ATTR\`): \`$OLD_HASH\` -> \`$NEW_HASH\`"$'\n' - - if [ "$MODE" = "--apply" ]; then - sed -i "s|hash = \"sha256-[^\"]*\";|hash = \"$NEW_HASH\";|" "$NIX_FILE" - nix build ".#$ATTR.npmDeps" --no-link --print-build-logs - FIXED=1 - echo " fixed" - fi - done - - if [ -n "''${GITHUB_OUTPUT:-}" ]; then - { - [ "$STALE" -eq 1 ] && echo "stale=true" || echo "stale=false" - [ "$FIXED" -eq 1 ] && echo "changed=true" || echo "changed=false" - if [ -n "$REPORT" ]; then - echo "report<> "$GITHUB_OUTPUT" - fi - - if [ "$STALE" -eq 1 ] && [ "$MODE" = "--check" ]; then - echo - echo "Stale lockfile hashes detected. Run:" - echo " nix run .#fix-lockfiles -- --apply" - exit 1 - fi - - exit 0 - ''; -} diff --git a/nix/packages.nix b/nix/packages.nix index 721546851..912be7843 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -8,12 +8,8 @@ inherit (inputs) uv2nix pyproject-nix pyproject-build-systems; }; - hermesNpmLib = pkgs.callPackage ./lib.nix { - npm-lockfile-fix = inputs'.npm-lockfile-fix.packages.default; - }; - hermesTui = pkgs.callPackage ./tui.nix { - inherit hermesNpmLib; + npm-lockfile-fix = inputs'.npm-lockfile-fix.packages.default; }; # Import bundled skills, excluding runtime caches @@ -23,7 +19,7 @@ }; hermesWeb = pkgs.callPackage ./web.nix { - inherit hermesNpmLib; + npm-lockfile-fix = inputs'.npm-lockfile-fix.packages.default; }; runtimeDeps = with pkgs; [ @@ -115,10 +111,6 @@ tui = hermesTui; web = hermesWeb; - - fix-lockfiles = hermesNpmLib.mkFixLockfiles { - packages = [ hermesTui hermesWeb ]; - }; }; }; } diff --git a/nix/tui.nix b/nix/tui.nix index 66658bb42..7303edecb 100644 --- a/nix/tui.nix +++ b/nix/tui.nix @@ -1,14 +1,16 @@ # nix/tui.nix — Hermes TUI (Ink/React) compiled with tsc and bundled -{ pkgs, hermesNpmLib, ... }: +{ pkgs, npm-lockfile-fix, ... }: let src = ../ui-tui; npmDeps = pkgs.fetchNpmDeps { inherit src; - hash = "sha256-BlxkTyn1x7ZQcj7pcMB5y5C2AyToT/CzxmtacTfEXmY="; + hash = "sha256-mG3vpgGi4ljt4X3XIf3I/5mIcm+rVTUAmx2DQ6YVA90="; }; packageJson = builtins.fromJSON (builtins.readFile (src + "/package.json")); version = packageJson.version; + + npmLockHash = builtins.hashString "sha256" (builtins.readFile ../ui-tui/package-lock.json); in pkgs.buildNpmPackage { pname = "hermes-tui"; @@ -16,12 +18,6 @@ pkgs.buildNpmPackage { doCheck = false; - patchPhase = '' - runHook prePatch - sed -i -z 's/\n$//' package-lock.json - runHook postPatch - ''; - installPhase = '' runHook preInstall @@ -43,23 +39,39 @@ pkgs.buildNpmPackage { ''; nativeBuildInputs = [ - (hermesNpmLib.mkUpdateLockfileScript { - name = "update_tui_lockfile"; - folder = "ui-tui"; - nixFile = "nix/tui.nix"; - attr = "tui"; - }) + (pkgs.writeShellScriptBin "update_tui_lockfile" '' + set -euox pipefail + + # get root of repo + REPO_ROOT=$(git rev-parse --show-toplevel) + + # cd into ui-tui and reinstall + cd "$REPO_ROOT/ui-tui" + rm -rf node_modules/ + npm cache clean --force + CI=true npm install # ci env var to suppress annoying unicode install banner lag + ${pkgs.lib.getExe npm-lockfile-fix} ./package-lock.json + + NIX_FILE="$REPO_ROOT/nix/tui.nix" + # compute the new hash + sed -i "s/hash = \"[^\"]*\";/hash = \"\";/" $NIX_FILE + NIX_OUTPUT=$(nix build .#tui 2>&1 || true) + NEW_HASH=$(echo "$NIX_OUTPUT" | grep 'got:' | awk '{print $2}') + echo got new hash $NEW_HASH + sed -i "s|hash = \"[^\"]*\";|hash = \"$NEW_HASH\";|" $NIX_FILE + nix build .#tui + echo "Updated npm hash in $NIX_FILE to $NEW_HASH" + '') ]; - passthru = { - devShellHook = hermesNpmLib.mkNpmDevShellHook { - name = "hermes-tui"; - folder = "ui-tui"; - }; - npmLockfile = { - attr = "tui"; - folder = "ui-tui"; - nixFile = "nix/tui.nix"; - }; - }; + passthru.devShellHook = '' + STAMP=".nix-stamps/hermes-tui" + STAMP_VALUE="${npmLockHash}" + if [ ! -f "$STAMP" ] || [ "$(cat "$STAMP")" != "$STAMP_VALUE" ]; then + echo "hermes-tui: installing npm dependencies..." + cd ui-tui && CI=true npm install --silent --no-fund --no-audit 2>/dev/null && cd .. + mkdir -p .nix-stamps + echo "$STAMP_VALUE" > "$STAMP" + fi + ''; } diff --git a/nix/web.nix b/nix/web.nix index 3926ed9ed..247889753 100644 --- a/nix/web.nix +++ b/nix/web.nix @@ -1,11 +1,13 @@ # nix/web.nix — Hermes Web Dashboard (Vite/React) frontend build -{ pkgs, hermesNpmLib, ... }: +{ pkgs, npm-lockfile-fix, ... }: let src = ../web; npmDeps = pkgs.fetchNpmDeps { inherit src; hash = "sha256-Y0pOzdFG8BLjfvCLmsvqYpjxFjAQabXp1i7X9W/cCU4="; }; + + npmLockHash = builtins.hashString "sha256" (builtins.readFile ../web/package-lock.json); in pkgs.buildNpmPackage { pname = "hermes-web"; @@ -26,23 +28,36 @@ pkgs.buildNpmPackage { ''; nativeBuildInputs = [ - (hermesNpmLib.mkUpdateLockfileScript { - name = "update_web_lockfile"; - folder = "web"; - nixFile = "nix/web.nix"; - attr = "web"; - }) + (pkgs.writeShellScriptBin "update_web_lockfile" '' + set -euox pipefail + + REPO_ROOT=$(git rev-parse --show-toplevel) + + cd "$REPO_ROOT/web" + rm -rf node_modules/ + npm cache clean --force + CI=true npm install + ${pkgs.lib.getExe npm-lockfile-fix} ./package-lock.json + + NIX_FILE="$REPO_ROOT/nix/web.nix" + sed -i "s/hash = \"[^\"]*\";/hash = \"\";/" $NIX_FILE + NIX_OUTPUT=$(nix build .#web 2>&1 || true) + NEW_HASH=$(echo "$NIX_OUTPUT" | grep 'got:' | awk '{print $2}') + echo got new hash $NEW_HASH + sed -i "s|hash = \"[^\"]*\";|hash = \"$NEW_HASH\";|" $NIX_FILE + nix build .#web + echo "Updated npm hash in $NIX_FILE to $NEW_HASH" + '') ]; - passthru = { - devShellHook = hermesNpmLib.mkNpmDevShellHook { - name = "hermes-web"; - folder = "web"; - }; - npmLockfile = { - attr = "web"; - folder = "web"; - nixFile = "nix/web.nix"; - }; - }; + passthru.devShellHook = '' + STAMP=".nix-stamps/hermes-web" + STAMP_VALUE="${npmLockHash}" + if [ ! -f "$STAMP" ] || [ "$(cat "$STAMP")" != "$STAMP_VALUE" ]; then + echo "hermes-web: installing npm dependencies..." + cd web && CI=true npm install --silent --no-fund --no-audit 2>/dev/null && cd .. + mkdir -p .nix-stamps + echo "$STAMP_VALUE" > "$STAMP" + fi + ''; }