Skip to content

Commit 61e668e

Browse files
author
Forge
committed
fix(release): make native checksum verification portable and race probes deterministic
1 parent 90516f8 commit 61e668e

7 files changed

Lines changed: 29 additions & 5 deletions

File tree

‎.github/workflows/release-candidate.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
timeout-minutes: 40
2626
env:
2727
CODEGRAPH_TELEMETRY: '0'
28+
NATIVE_VALIDATION_SCOPE: 'repairs'
2829
steps:
2930
- uses: actions/checkout@v4
3031
with:

‎__tests__/db-reopen-on-replace.test.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,16 @@ describe('CodeGraph.reopenIfReplaced (issue #925)', () => {
122122
let finishOpen!: (connection: DatabaseConnection) => void;
123123
const openGate = new Promise<DatabaseConnection>((resolve) => { finishOpen = resolve; });
124124
const replacedSpy = vi.spyOn(stale, 'isReplacedOnDisk').mockReturnValue(true);
125-
const openSpy = vi.spyOn(DatabaseConnection, 'openAsync').mockReturnValue(openGate);
125+
let enteredOpen!: () => void;
126+
const opening = new Promise<void>((resolve) => { enteredOpen = resolve; });
127+
const openSpy = vi.spyOn(DatabaseConnection, 'openAsync').mockImplementation(() => {
128+
enteredOpen();
129+
return openGate;
130+
});
126131

127132
try {
128133
const reopening = server.reopenIfReplacedAsync();
134+
await opening; // The guard may yield before the database open starts.
129135
server.close();
130136
finishOpen(fresh);
131137

‎install.ps1‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ $entries = @(Get-Content -LiteralPath $sumsPath | Where-Object { $_.Trim() -matc
4545
if ($entries.Count -ne 1) { throw 'codegraph: missing or ambiguous archive checksum.' }
4646
$null = $entries[0].Trim() -match $pattern
4747
$expectedHash = $Matches[1]
48-
if ((Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash -ne $expectedHash) { throw 'codegraph: archive checksum mismatch; installation unchanged.' }
48+
# Use .NET directly: Windows PowerShell launched from pwsh can lack Get-FileHash.
49+
$hasher = [System.Security.Cryptography.SHA256]::Create()
50+
$stream = [System.IO.File]::OpenRead($zip)
51+
try { $actualHash = [BitConverter]::ToString($hasher.ComputeHash($stream)).Replace('-', '') }
52+
finally { $stream.Dispose(); $hasher.Dispose() }
53+
if ($actualHash -ne $expectedHash) { throw 'codegraph: archive checksum mismatch; installation unchanged.' }
4954

5055

5156
$dest = Join-Path $installDir 'current'

‎scripts/validation/release-installers.cjs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ try {
3939
result=run('powershell.exe',['-NoProfile','-ExecutionPolicy','Bypass','-File',script],env);
4040
}else result=run('sh',[path.join(repo,'install.sh')],env);
4141
assert.equal(result.status===0,mode==='valid',result.stdout+'\n'+result.stderr);
42+
if(mode==='mismatch') assert.match(result.stdout+'\n'+result.stderr,/checksum mismatch/i);
43+
if(mode==='duplicate'||mode==='unlisted') assert.match(result.stdout+'\n'+result.stderr,/missing or ambiguous|checksum.*(missing|duplicate)|expected.*checksum/i);
44+
if(mode==='missing'&&windows) assert.match(result.stdout+'\n'+result.stderr,/fixture 404/);
4245
assert.equal(fs.readFileSync(path.join(dest,'bin','codegraph'),'utf8'),mode==='valid'?'replacement':'original');
4346
});
4447
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}));

‎scripts/validation/release-native.cjs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@ async function run(name, command, args, timeout = 360000, overrides = {}) {
3737
}
3838
(async () => {
3939
const built = await run('build', ...npmCommand(['run', 'build']));
40-
if (built) {
40+
if (built && metadata.scope === 'repairs') {
41+
metadata.notRun = ['unchanged core/worker/lifecycle paths: reuse successful steps at 90516f8, run 35880834124'];
42+
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);
43+
await run('file-growth-race', process.execPath, ['scripts/validation/release-size-race.cjs','dist']);
44+
await run('installer-archives', process.execPath, ['scripts/validation/release-installers.cjs']);
45+
await run('security-negative', process.execPath, ['scripts/validation/release-security.cjs','.','dist']);
46+
} else if (built) {
4147
await run('focused', process.execPath, ['node_modules/vitest/vitest.mjs', 'run',
4248
'__tests__/watcher-replaced-db.test.ts', '__tests__/schemaless-db-not-initialized.test.ts', '__tests__/liveness-watchdog.test.ts',
4349
'__tests__/mpeg-ts-not-typescript.test.ts', '__tests__/oversize-file-not-read.test.ts', '__tests__/bounded-source.test.ts',
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const fs=require('fs'),fsp=require('fs/promises'),path=require('path'),os=require('os'),assert=require('assert/strict');
22
const {CodeGraph}=require(path.resolve(process.argv[2]||'dist','index.js'));
3-
(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;
3+
(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;
44
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;};
55
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;});

‎src/upgrade/index.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,10 @@ export function buildWindowsUpgradeScript(bundleRoot: string, version: string, a
598598
`if($entries.Count -ne 1){throw 'Missing or ambiguous archive checksum'}`,
599599
`$null=$entries[0].Trim() -match $pattern`,
600600
`$expectedHash=$Matches[1]`,
601-
`if((Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash -ne $expectedHash){throw 'Archive checksum mismatch; installation unchanged'}`,
601+
`$hasher=[System.Security.Cryptography.SHA256]::Create()`,
602+
`$stream=[System.IO.File]::OpenRead($zip)`,
603+
`try{$actualHash=[BitConverter]::ToString($hasher.ComputeHash($stream)).Replace('-','')}finally{$stream.Dispose();$hasher.Dispose()}`,
604+
`if($actualHash -ne $expectedHash){throw 'Archive checksum mismatch; installation unchanged'}`,
602605
`$stage=Join-Path $tmp 'stage'`,
603606
`Expand-Archive -Path $zip -DestinationPath $stage -Force`,
604607
`$inner=Join-Path $stage 'codegraph-${target}'`,

0 commit comments

Comments
 (0)