Skip to content

Commit 27c4bdf

Browse files
authored
Merge branch 'main' into changeset-release/main
2 parents c0db15b + 24ce17c commit 27c4bdf

3 files changed

Lines changed: 41 additions & 24 deletions

File tree

apps/pythinker-code/test/scripts/check-nix-hash-fresh.test.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,17 @@ describe('check-nix-hash-fresh', () => {
4646

4747
const result = runCheck(root);
4848
expect(result.status, result.stderr).toBe(1);
49-
expect(result.stderr).toContain('pnpm-lock.yaml changed on this branch but flake.nix did not');
49+
expect(result.stderr).toContain('could not verify the committed dependency hash');
50+
});
51+
52+
it('accepts a lock-only change when the dependency rebuild verifies its hash', () => {
53+
const root = makeRepository('feature');
54+
setRemoteRef(root, 'main', revParse(root, 'HEAD'));
55+
writeFileSync(join(root, 'pnpm-lock.yaml'), 'equivalent dependency closure\n');
56+
commit(root, 'update lock without changing fetched dependencies');
57+
58+
const result = runCheck(root, 0);
59+
expect(result.status, result.stderr).toBe(0);
5060
});
5161
});
5262

@@ -84,6 +94,11 @@ function git(root: string, args: string[]): string {
8494
return execFileSync('git', args, { cwd: root, encoding: 'utf8' }).trim();
8595
}
8696

87-
function runCheck(root: string): SpawnSyncReturns<string> {
88-
return spawnSync(process.execPath, [checkScript], { cwd: root, encoding: 'utf8' });
97+
function runCheck(root: string, nixExitCode = 1): SpawnSyncReturns<string> {
98+
writeFileSync(join(root, 'nix'), `#!/bin/sh\nexit ${nixExitCode}\n`, { mode: 0o755 });
99+
return spawnSync(process.execPath, [checkScript], {
100+
cwd: root,
101+
encoding: 'utf8',
102+
env: { ...process.env, PATH: `${root}:${process.env['PATH']}` },
103+
});
89104
}

scripts/check-nix-hash-fresh.mjs

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
11
#!/usr/bin/env node
2-
// Cheap proxy for the Nix fetchPnpmDeps hash going stale.
3-
//
4-
// flake.nix pins a `hash = "sha256-..."` for pkgs.fetchPnpmDeps, computed
5-
// from pnpm-lock.yaml. If the lockfile changes and nobody refreshes that
6-
// hash, `nix build` fails in CI with a hash mismatch. Recomputing the real
7-
// hash requires `nix`, which may not be installed locally, so this checks a
8-
// proxy instead: did pnpm-lock.yaml change on this branch without flake.nix
9-
// also changing? That's not proof the hash is stale (a flake.nix edit could
10-
// be unrelated), but it's the cheap signal that catches the common case.
11-
//
12-
// ponytail: proxy check, not a real hash recompute — upgrade to `nix build
13-
// --dry-run` locally if false positives/negatives become a problem.
2+
// Use the branch diff as a fast path, then verify inconclusive lock-only changes
3+
// against the committed dependency derivation. Rebuild fixed-output dependencies
4+
// so a cached store path cannot hide a stale hash.
145
import { execFileSync } from 'node:child_process';
6+
import { pathToFileURL } from 'node:url';
157

168
function git(args) {
179
return execFileSync('git', args, { encoding: 'utf8' }).trim();
@@ -60,10 +52,20 @@ if (!lockChanged || flakeChanged) {
6052
process.exit(0);
6153
}
6254

63-
console.error(
64-
'❌ pnpm-lock.yaml changed on this branch but flake.nix did not.\n' +
65-
' flake.nix pins a fetchPnpmDeps hash of pnpm-lock.yaml, so its hash may be stale.\n' +
66-
' Refresh it: run a nix build (e.g. `nix build .#pythinker-code`), take the sha256-... hash\n' +
67-
' from the mismatch error, paste it into the `hash = "sha256-...";` line in flake.nix, commit, and re-push.',
68-
);
69-
process.exit(1);
55+
console.log('[nix-hash-freshness] lockfile changed without flake.nix; verifying the committed dependency hash.');
56+
try {
57+
const source = pathToFileURL(git(['rev-parse', '--show-toplevel']));
58+
source.searchParams.set('rev', git(['rev-parse', 'HEAD']));
59+
source.hash = 'pythinker-code.pnpmDeps';
60+
execFileSync('nix', ['build', `git+${source.href}`, '--rebuild', '--no-link'], {
61+
stdio: 'inherit',
62+
timeout: 600_000,
63+
});
64+
} catch (error) {
65+
console.error(
66+
'[nix-hash-freshness] could not verify the committed dependency hash. ' +
67+
'Install Nix if unavailable, resolve build errors, or refresh flake.nix with the reported hash.',
68+
);
69+
console.error(error.message);
70+
process.exit(1);
71+
}

scripts/pre-push.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ step "lint" pnpm run lint || fail "lint" "oxlint reported errors"
7171
step "web-bundle-freshness" node apps/pythinker-code/scripts/check-web-assets.mjs ||
7272
fail "build" "apps/pythinker-web changed without restaging dist-web (run 'pnpm run build:web')"
7373

74-
# 5. Nix fetchPnpmDeps hash freshness proxy (CI: nix build).
74+
# 5. Nix fetchPnpmDeps hash freshness (CI: nix build).
7575
step "nix-hash-freshness" node scripts/check-nix-hash-fresh.mjs ||
76-
fail "nix build (flake.nix)" "pnpm-lock.yaml changed without refreshing flake.nix's pnpmDeps hash"
76+
fail "nix build (flake.nix)" "could not verify flake.nix's pnpmDeps hash (see Nix output above)"
7777

7878
# 6. Typecheck, scoped to packages changed since $base_ref (CI: `pnpm run
7979
# typecheck`). Full typecheck builds every package first and takes

0 commit comments

Comments
 (0)