@@ -34,6 +34,7 @@ async function writeFixtureInstaller(directory, version, options = {}) {
3434 includeMetadata = true ,
3535 additionalMetadata = [ ] ,
3636 assetSize = 1024 ,
37+ includeBlockmap = false ,
3738 } = options ;
3839
3940 const releaseDir = path . join ( directory , 'release-artifacts' , artifactDir , 'release' ) ;
@@ -43,6 +44,12 @@ async function writeFixtureInstaller(directory, version, options = {}) {
4344 const binary = crypto . randomBytes ( assetSize ) ;
4445 await fs . writeFile ( installerPath , binary ) ;
4546
47+ let blockmapPath = null ;
48+ if ( includeBlockmap ) {
49+ blockmapPath = `${ installerPath } .blockmap` ;
50+ await fs . writeFile ( blockmapPath , crypto . randomBytes ( Math . max ( 256 , Math . floor ( assetSize / 4 ) ) ) ) ;
51+ }
52+
4653 let metadataPath = null ;
4754 if ( includeMetadata && metadataFileName ) {
4855 const metadata = {
@@ -84,6 +91,7 @@ async function writeFixtureInstaller(directory, version, options = {}) {
8491 artifactDir,
8592 assetPath : installerPath ,
8693 assetName,
94+ blockmapPath,
8795 } ;
8896}
8997
@@ -148,10 +156,10 @@ async function runGenerateReleaseNotes({
148156 ) ;
149157}
150158
151- async function runLocalVerification ( directory ) {
159+ async function runLocalVerification ( directory , extraArgs = [ ] ) {
152160 await execFileAsync (
153161 'node' ,
154- [ repoPath ( 'scripts' , 'test-auto-update.mjs' ) , '--local' , directory ] ,
162+ [ repoPath ( 'scripts' , 'test-auto-update.mjs' ) , '--local' , directory , ... extraArgs ] ,
155163 {
156164 cwd : repoPath ( ) ,
157165 } ,
@@ -230,11 +238,89 @@ test('release tooling rewrites metadata and keeps latest.yml published', async (
230238 await runLocalVerification ( releaseDir ) ;
231239} ) ;
232240
241+ test ( 'release tooling ignores unpacked directories when enforcing metadata' , async ( t ) => {
242+ const workspace = await createTemporaryWorkspace ( t ) ;
243+ await writeFixtureInstaller ( workspace , '0.0.2' , {
244+ artifactDir : 'docforge-windows-x64' ,
245+ } ) ;
246+
247+ const unpackedDir = path . join (
248+ workspace ,
249+ 'release-artifacts' ,
250+ 'docforge-windows-x64' ,
251+ 'win-unpacked' ,
252+ ) ;
253+ await fs . mkdir ( unpackedDir , { recursive : true } ) ;
254+ const unpackedExecutable = path . join ( unpackedDir , 'DocForge.exe' ) ;
255+ await fs . writeFile ( unpackedExecutable , crypto . randomBytes ( 2048 ) ) ;
256+
257+ const changelogPath = path . join ( workspace , 'CHANGELOG.md' ) ;
258+ await fs . writeFile (
259+ changelogPath ,
260+ [ '## v0.0.2' , '' , '- Ignore unpacked executable fixtures.' ] . join ( '\n' ) ,
261+ 'utf8' ,
262+ ) ;
263+
264+ const notesPath = path . join ( workspace , 'release-notes.md' ) ;
265+ const manifestPath = path . join ( workspace , 'release-files.txt' ) ;
266+
267+ await runGenerateReleaseNotes ( {
268+ workspace,
269+ version : '0.0.2' ,
270+ tag : 'v0.0.2' ,
271+ changelogPath,
272+ outputPath : notesPath ,
273+ filesOutputPath : manifestPath ,
274+ } ) ;
275+
276+ const manifest = await fs . readFile ( manifestPath , 'utf8' ) ;
277+ const entries = readManifestEntries ( manifest ) ;
278+ assert ( entries . every ( ( line ) => ! line . includes ( 'DocForge.exe' ) ) ) ;
279+ } ) ;
280+
281+ test ( 'local verification can repair mismatched metadata when requested' , async ( t ) => {
282+ const workspace = await createTemporaryWorkspace ( t ) ;
283+ const version = '0.0.2' ;
284+ const { releaseDir, metadataPath, assetPath } = await writeFixtureInstaller ( workspace , version , {
285+ assetSize : 2048 ,
286+ } ) ;
287+
288+ const corrupted = YAML . parse ( await fs . readFile ( metadataPath , 'utf8' ) ) ;
289+ corrupted . sha512 = 'invalid-sha512' ;
290+ corrupted . files [ 0 ] . sha512 = 'invalid-sha512' ;
291+ corrupted . files [ 0 ] . size = 1 ;
292+ await fs . writeFile ( metadataPath , YAML . stringify ( corrupted ) , 'utf8' ) ;
293+
294+ await assert . rejects ( ( ) => runLocalVerification ( releaseDir ) , / L o c a l a u t o - u p d a t e v e r i f i c a t i o n f a i l e d / ) ;
295+
296+ await runLocalVerification ( releaseDir , [ '--fix-metadata' ] ) ;
297+
298+ const installerBuffer = await fs . readFile ( assetPath ) ;
299+ const expectedSha = computeSha512Base64 ( installerBuffer ) ;
300+ const updated = YAML . parse ( await fs . readFile ( metadataPath , 'utf8' ) ) ;
301+
302+ assert . equal ( updated . path , path . basename ( assetPath ) ) ;
303+ assert . equal ( updated . sha512 , expectedSha ) ;
304+ if ( Object . prototype . hasOwnProperty . call ( updated , 'size' ) ) {
305+ assert . equal ( updated . size , installerBuffer . length ) ;
306+ }
307+ assert ( Array . isArray ( updated . files ) && updated . files . length === 1 ) ;
308+ assert . equal ( updated . files [ 0 ] . url , path . basename ( assetPath ) ) ;
309+ assert . equal ( updated . files [ 0 ] . sha512 , expectedSha ) ;
310+ assert . equal ( updated . files [ 0 ] . size , installerBuffer . length ) ;
311+ } ) ;
312+
233313test ( 'metadata updates remain isolated across artifact directories with identical installer names' , async ( t ) => {
234314 const workspace = await createTemporaryWorkspace ( t ) ;
235315 const version = '0.0.3' ;
236- const x64 = await writeFixtureInstaller ( workspace , version , { artifactDir : 'docforge-windows-x64' } ) ;
237- const arm64 = await writeFixtureInstaller ( workspace , version , { artifactDir : 'docforge-windows-arm64' } ) ;
316+ const x64 = await writeFixtureInstaller ( workspace , version , {
317+ artifactDir : 'docforge-windows-x64' ,
318+ includeBlockmap : true ,
319+ } ) ;
320+ const arm64 = await writeFixtureInstaller ( workspace , version , {
321+ artifactDir : 'docforge-windows-arm64' ,
322+ includeBlockmap : true ,
323+ } ) ;
238324
239325 const changelogPath = path . join ( workspace , 'CHANGELOG.md' ) ;
240326 await fs . writeFile (
@@ -255,13 +341,16 @@ test('metadata updates remain isolated across artifact directories with identica
255341 filesOutputPath : manifestPath ,
256342 } ) ;
257343
258- const renamedInstallerName = `DocForge-Setup-${ version } .exe` ;
259- const renamedX64 = path . join ( x64 . releaseDir , renamedInstallerName ) ;
260- const renamedArm64 = path . join ( arm64 . releaseDir , renamedInstallerName ) ;
344+ const renamedX64Name = `DocForge-Setup-${ version } .exe` ;
345+ const renamedArm64Name = `DocForge-Setup-${ version } -arm64.exe` ;
346+ const renamedX64 = path . join ( x64 . releaseDir , renamedX64Name ) ;
347+ const renamedArm64 = path . join ( arm64 . releaseDir , renamedArm64Name ) ;
261348
262349 await Promise . all ( [
263350 assert . doesNotReject ( ( ) => fs . access ( renamedX64 ) ) ,
264351 assert . doesNotReject ( ( ) => fs . access ( renamedArm64 ) ) ,
352+ assert . doesNotReject ( ( ) => fs . access ( path . join ( x64 . releaseDir , `${ renamedX64Name } .blockmap` ) ) ) ,
353+ assert . doesNotReject ( ( ) => fs . access ( path . join ( arm64 . releaseDir , `${ renamedArm64Name } .blockmap` ) ) ) ,
265354 ] ) ;
266355
267356 const [ x64Buffer , arm64Buffer ] = await Promise . all ( [
@@ -286,20 +375,20 @@ test('metadata updates remain isolated across artifact directories with identica
286375 assert ( entries . includes ( expectedX64Entry ) , 'x64 installer should be listed in manifest' ) ;
287376 assert ( entries . includes ( expectedArm64Entry ) , 'arm64 installer should be listed in manifest' ) ;
288377
289- const assertMetadataMatches = ( metadata , expectedSha , bufferLength ) => {
290- assert . equal ( metadata . path , renamedInstallerName ) ;
378+ const assertMetadataMatches = ( metadata , expectedName , expectedSha , bufferLength ) => {
379+ assert . equal ( metadata . path , expectedName ) ;
291380 assert . equal ( metadata . sha512 , expectedSha ) ;
292381 if ( Object . prototype . hasOwnProperty . call ( metadata , 'size' ) ) {
293382 assert . equal ( metadata . size , bufferLength ) ;
294383 }
295384 assert ( Array . isArray ( metadata . files ) && metadata . files . length === 1 ) ;
296- assert . equal ( metadata . files [ 0 ] . url , renamedInstallerName ) ;
385+ assert . equal ( metadata . files [ 0 ] . url , expectedName ) ;
297386 assert . equal ( metadata . files [ 0 ] . sha512 , expectedSha ) ;
298387 assert . equal ( metadata . files [ 0 ] . size , bufferLength ) ;
299388 } ;
300389
301- assertMetadataMatches ( x64Metadata , x64Sha , x64Buffer . length ) ;
302- assertMetadataMatches ( arm64Metadata , arm64Sha , arm64Buffer . length ) ;
390+ assertMetadataMatches ( x64Metadata , renamedX64Name , x64Sha , x64Buffer . length ) ;
391+ assertMetadataMatches ( arm64Metadata , renamedArm64Name , arm64Sha , arm64Buffer . length ) ;
303392
304393 await Promise . all ( [
305394 runLocalVerification ( x64 . releaseDir ) ,
@@ -313,6 +402,7 @@ test('release workflow verifies metadata directories across platforms and publis
313402
314403 const ia32 = await writeFixtureInstaller ( workspace , version , {
315404 artifactDir : 'docforge-windows-ia32' ,
405+ includeBlockmap : true ,
316406 additionalMetadata : [
317407 {
318408 relativePath : path . join ( 'win-ia32-unpacked' , 'resources' , 'app-update.yml' ) ,
@@ -326,6 +416,7 @@ test('release workflow verifies metadata directories across platforms and publis
326416
327417 const x64 = await writeFixtureInstaller ( workspace , version , {
328418 artifactDir : 'docforge-windows-x64' ,
419+ includeBlockmap : true ,
329420 } ) ;
330421
331422 const linux = await writeFixtureInstaller ( workspace , version , {
@@ -360,6 +451,43 @@ test('release workflow verifies metadata directories across platforms and publis
360451 } ) ;
361452
362453 const manifestEntries = new Set ( readManifestEntries ( await fs . readFile ( manifestPath , 'utf8' ) ) ) ;
454+ const publishedNames = new Map ( ) ;
455+ for ( const entry of manifestEntries ) {
456+ const [ relativePath , explicitName ] = entry . split ( '#' ) ;
457+ const candidateName = explicitName || path . basename ( relativePath ) ;
458+ const previousEntry = publishedNames . get ( candidateName ) ;
459+ assert (
460+ ! previousEntry ,
461+ `Duplicate release asset name detected: ${ candidateName } (entries: ${ previousEntry } , ${ entry } )` ,
462+ ) ;
463+ publishedNames . set ( candidateName , entry ) ;
464+ }
465+ const appUpdateRelative = path . relative (
466+ repoPath ( ) ,
467+ path . join ( ia32 . releaseDir , 'win-ia32-unpacked' , 'resources' , 'app-update.yml' ) ,
468+ ) ;
469+ assert (
470+ ! manifestEntries . has ( appUpdateRelative ) ,
471+ 'app-update.yml should not be uploaded to the release' ,
472+ ) ;
473+ const ia32InstallerName = `DocForge-Setup-${ version } -ia32.exe` ;
474+ const x64InstallerName = `DocForge-Setup-${ version } .exe` ;
475+ const expectedWindowsBinaries = [
476+ path . relative ( repoPath ( ) , path . join ( ia32 . releaseDir , ia32InstallerName ) ) ,
477+ path . relative ( repoPath ( ) , path . join ( x64 . releaseDir , x64InstallerName ) ) ,
478+ ] ;
479+ for ( const entry of expectedWindowsBinaries ) {
480+ assert ( manifestEntries . has ( entry ) , `${ entry } must be included for Windows release assets` ) ;
481+ }
482+
483+ const expectedBlockmaps = [
484+ path . relative ( repoPath ( ) , path . join ( ia32 . releaseDir , `${ ia32InstallerName } .blockmap` ) ) ,
485+ path . relative ( repoPath ( ) , path . join ( x64 . releaseDir , `${ x64InstallerName } .blockmap` ) ) ,
486+ ] ;
487+ for ( const blockmap of expectedBlockmaps ) {
488+ assert ( manifestEntries . has ( blockmap ) , `${ blockmap } must be published after renaming installers` ) ;
489+ }
490+
363491 const expectedMetadataUploads = [ x64 . metadataPath , linux . metadataPath , mac . metadataPath ] ;
364492 for ( const metadataPath of expectedMetadataUploads ) {
365493 assert ( metadataPath , 'metadataPath should be defined for uploaded manifests' ) ;
0 commit comments