fix(release): make native checksum verification portable and race probes deterministic

This commit is contained in:
Forge
2026-09-23 19:54:49 +00:00
parent 90516f8b9a
commit 61e668e613
7 changed files with 29 additions and 5 deletions
+1
View File
@@ -25,6 +25,7 @@ jobs:
timeout-minutes: 40
env:
CODEGRAPH_TELEMETRY: '0'
NATIVE_VALIDATION_SCOPE: 'repairs'
steps:
- uses: actions/checkout@v4
with:
+7 -1
View File
@@ -122,10 +122,16 @@ describe('CodeGraph.reopenIfReplaced (issue #925)', () => {
let finishOpen!: (connection: DatabaseConnection) => void;
const openGate = new Promise<DatabaseConnection>((resolve) => { finishOpen = resolve; });
const replacedSpy = vi.spyOn(stale, 'isReplacedOnDisk').mockReturnValue(true);
const openSpy = vi.spyOn(DatabaseConnection, 'openAsync').mockReturnValue(openGate);
let enteredOpen!: () => void;
const opening = new Promise<void>((resolve) => { enteredOpen = resolve; });
const openSpy = vi.spyOn(DatabaseConnection, 'openAsync').mockImplementation(() => {
enteredOpen();
return openGate;
});
try {
const reopening = server.reopenIfReplacedAsync();
await opening; // The guard may yield before the database open starts.
server.close();
finishOpen(fresh);
+6 -1
View File
@@ -45,7 +45,12 @@ $entries = @(Get-Content -LiteralPath $sumsPath | Where-Object { $_.Trim() -matc
if ($entries.Count -ne 1) { throw 'codegraph: missing or ambiguous archive checksum.' }
$null = $entries[0].Trim() -match $pattern
$expectedHash = $Matches[1]
if ((Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash -ne $expectedHash) { throw 'codegraph: archive checksum mismatch; installation unchanged.' }
# Use .NET directly: Windows PowerShell launched from pwsh can lack Get-FileHash.
$hasher = [System.Security.Cryptography.SHA256]::Create()
$stream = [System.IO.File]::OpenRead($zip)
try { $actualHash = [BitConverter]::ToString($hasher.ComputeHash($stream)).Replace('-', '') }
finally { $stream.Dispose(); $hasher.Dispose() }
if ($actualHash -ne $expectedHash) { throw 'codegraph: archive checksum mismatch; installation unchanged.' }
$dest = Join-Path $installDir 'current'
@@ -39,6 +39,9 @@ try {
result=run('powershell.exe',['-NoProfile','-ExecutionPolicy','Bypass','-File',script],env);
}else result=run('sh',[path.join(repo,'install.sh')],env);
assert.equal(result.status===0,mode==='valid',result.stdout+'\n'+result.stderr);
if(mode==='mismatch') assert.match(result.stdout+'\n'+result.stderr,/checksum mismatch/i);
if(mode==='duplicate'||mode==='unlisted') assert.match(result.stdout+'\n'+result.stderr,/missing or ambiguous|checksum.*(missing|duplicate)|expected.*checksum/i);
if(mode==='missing'&&windows) assert.match(result.stdout+'\n'+result.stderr,/fixture 404/);
assert.equal(fs.readFileSync(path.join(dest,'bin','codegraph'),'utf8'),mode==='valid'?'replacement':'original');
});
console.log(JSON.stringify({platform:process.platform,node:process.version,scope:windows?'native PowerShell install-prefix and full generated upgrade':'actual shell installer with inert download stubs',rows}));
+7 -1
View File
@@ -37,7 +37,13 @@ async function run(name, command, args, timeout = 360000, overrides = {}) {
}
(async () => {
const built = await run('build', ...npmCommand(['run', 'build']));
if (built) {
if (built && metadata.scope === 'repairs') {
metadata.notRun = ['unchanged core/worker/lifecycle paths: reuse successful steps at 90516f8, run 35880834124'];
await run('focused', process.execPath, ['node_modules/vitest/vitest.mjs', 'run', '__tests__/db-reopen-on-replace.test.ts', '__tests__/upgrade.test.ts', '__tests__/cli-ui-command.test.ts', '--maxWorkers=1', '--minWorkers=1', '--reporter=default', '--reporter=json', `--outputFile.json=${path.join(out, 'focused.json')}`], 300000);
await run('file-growth-race', process.execPath, ['scripts/validation/release-size-race.cjs','dist']);
await run('installer-archives', process.execPath, ['scripts/validation/release-installers.cjs']);
await run('security-negative', process.execPath, ['scripts/validation/release-security.cjs','.','dist']);
} else if (built) {
await run('focused', process.execPath, ['node_modules/vitest/vitest.mjs', 'run',
'__tests__/watcher-replaced-db.test.ts', '__tests__/schemaless-db-not-initialized.test.ts', '__tests__/liveness-watchdog.test.ts',
'__tests__/mpeg-ts-not-typescript.test.ts', '__tests__/oversize-file-not-read.test.ts', '__tests__/bounded-source.test.ts',
+1 -1
View File
@@ -1,5 +1,5 @@
const fs=require('fs'),fsp=require('fs/promises'),path=require('path'),os=require('os'),assert=require('assert/strict');
const {CodeGraph}=require(path.resolve(process.argv[2]||'dist','index.js'));
(async()=>{const root=fs.mkdtempSync(path.join(os.tmpdir(),'cg-size-race-'));const file=path.join(root,'grow.py');fs.writeFileSync(file,'# small');let grown=false;const stat=fsp.stat;let cg;
(async()=>{const root=fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(),'cg-size-race-')));const file=path.join(root,'grow.py');fs.writeFileSync(file,'# small');let grown=false;const stat=fsp.stat;let cg;
fsp.stat=async function(p,...a){const s=await stat(p,...a);if(String(p)===file&&!grown){grown=true;fs.writeFileSync(file,Buffer.alloc(1024*1024+1,35));}return s;};
try{cg=await CodeGraph.init(root,{index:true});const f=cg.getFiles().find(f=>f.path==='grow.py');console.log(JSON.stringify({grown,file:f,node:process.version}));assert(grown);assert.equal(f.size,1024*1024+1);assert(f.errors?.some(e=>e.code==='size_exceeded'));assert.equal(cg.getNodesInFile('grow.py').length,0);}finally{fsp.stat=stat;cg?.close();fs.rmSync(root,{recursive:true,force:true});}})().catch(e=>{console.error(e);process.exitCode=1;});
+4 -1
View File
@@ -598,7 +598,10 @@ export function buildWindowsUpgradeScript(bundleRoot: string, version: string, a
`if($entries.Count -ne 1){throw 'Missing or ambiguous archive checksum'}`,
`$null=$entries[0].Trim() -match $pattern`,
`$expectedHash=$Matches[1]`,
`if((Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash -ne $expectedHash){throw 'Archive checksum mismatch; installation unchanged'}`,
`$hasher=[System.Security.Cryptography.SHA256]::Create()`,
`$stream=[System.IO.File]::OpenRead($zip)`,
`try{$actualHash=[BitConverter]::ToString($hasher.ComputeHash($stream)).Replace('-','')}finally{$stream.Dispose();$hasher.Dispose()}`,
`if($actualHash -ne $expectedHash){throw 'Archive checksum mismatch; installation unchanged'}`,
`$stage=Join-Path $tmp 'stage'`,
`Expand-Archive -Path $zip -DestinationPath $stage -Force`,
`$inner=Join-Path $stage 'codegraph-${target}'`,