From 16c6a99ca24b9ad57faa6369ba4c58d0a5f2d421 Mon Sep 17 00:00:00 2001 From: lofyer Date: Wed, 5 Aug 2026 19:54:15 +0800 Subject: [PATCH] fix: publish verified release assets --- build/aggregate-release.cjs | 32 +++++++++++++++-- tests/aggregate-release.test.ts | 61 +++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/build/aggregate-release.cjs b/build/aggregate-release.cjs index 4d5f511..2369b9d 100644 --- a/build/aggregate-release.cjs +++ b/build/aggregate-release.cjs @@ -127,6 +127,20 @@ function expectedFormatForFile(name, target) { ) } +function isAllowedAuxiliaryFile(name, target, manifestNames) { + if (name === 'builder-debug.yml') { + return true + } + if (!name.endsWith('.blockmap')) { + return false + } + const packageName = name.slice(0, -'.blockmap'.length) + return ( + manifestNames.has(packageName) && + expectedFormatForFile(packageName, target) !== undefined + ) +} + function listTargetDirectories(inputDirectory) { if (lstatSync(inputDirectory).isSymbolicLink()) { throw new Error(`拒绝符号链接:${inputDirectory}`) @@ -210,6 +224,9 @@ async function aggregateRelease(inputDirectory, outputDirectory) { const seenFormats = new Set() const files = [] + const manifestNames = new Set( + item.manifest.files.map((file) => file?.name) + ) for (const file of item.manifest.files) { assertSafeName(file?.name, '发布文件名') if ( @@ -224,7 +241,12 @@ async function aggregateRelease(inputDirectory, outputDirectory) { throw new Error(`发布文件名全局重复:${file.name}`) } const format = expectedFormatForFile(file.name, expected) - if (!format || seenFormats.has(format)) { + const auxiliary = isAllowedAuxiliaryFile( + file.name, + expected, + manifestNames + ) + if ((!format && !auxiliary) || (format && seenFormats.has(format))) { throw new Error(`发布文件格式或数量错误:${file.name}`) } const source = join(directory, file.name) @@ -234,6 +256,9 @@ async function aggregateRelease(inputDirectory, outputDirectory) { if (actualSize !== file.size || actualHash !== file.sha256) { throw new Error(`发布文件完整性校验失败:${file.name}`) } + if (auxiliary) { + continue + } assertPlainFile(source, '发布文件') copyFileSync(source, join(outputDirectory, file.name)) fileNames.add(file.name) @@ -253,7 +278,10 @@ async function aggregateRelease(inputDirectory, outputDirectory) { const renamedManifest = `release-manifest-${key}.json` writeFileSync( join(outputDirectory, renamedManifest), - `${JSON.stringify(item.manifest, null, 2)}\n`, + `${JSON.stringify({ + ...item.manifest, + files + }, null, 2)}\n`, 'utf8' ) targets.push({ diff --git a/tests/aggregate-release.test.ts b/tests/aggregate-release.test.ts index 2a02fc4..cd22f4b 100644 --- a/tests/aggregate-release.test.ts +++ b/tests/aggregate-release.test.ts @@ -81,6 +81,30 @@ function createDownloadedArtifacts(parent: string): string { sha256: sha256(content) } }) + const debugContent = `${key}:debug` + writeFileSync( + join(directory, 'builder-debug.yml'), + debugContent + ) + files.push({ + name: 'builder-debug.yml', + size: Buffer.byteLength(debugContent), + sha256: sha256(debugContent) + }) + if (target.platform === 'windows') { + const setupName = artifactName(target, 'nsis') + const blockmapName = `${setupName}.blockmap` + const blockmapContent = `${key}:blockmap` + writeFileSync( + join(directory, blockmapName), + blockmapContent + ) + files.push({ + name: blockmapName, + size: Buffer.byteLength(blockmapContent), + sha256: sha256(blockmapContent) + }) + } writeFileSync( join(directory, 'release-manifest.json'), `${JSON.stringify({ @@ -124,6 +148,20 @@ describe('release asset aggregation', () => { expect(outputNames).toHaveLength(20) expect(outputNames).toContain('release-manifest.json') expect(outputNames).toContain('SHA256SUMS') + expect(outputNames).not.toContain('builder-debug.yml') + expect( + outputNames.some((name) => name.endsWith('.blockmap')) + ).toBe(false) + const windowsManifest = JSON.parse( + readFileSync( + join(output, 'release-manifest-windows-x64.json'), + 'utf8' + ) + ) as { files: Array<{ name: string }> } + expect(windowsManifest.files.map((file) => file.name)).toEqual([ + artifactName(aggregate.targetDefinitions[0]!, 'nsis'), + artifactName(aggregate.targetDefinitions[0]!, 'portable') + ]) const sums = readFileSync( join(output, 'SHA256SUMS'), 'utf8' @@ -159,6 +197,29 @@ describe('release asset aggregation', () => { } }) + it('verifies auxiliary files even though they are not published', async () => { + const parent = mkdtempSync( + join(tmpdir(), 'goodbuddy-release-auxiliary-') + ) + try { + const input = createDownloadedArtifacts(parent) + writeFileSync( + join( + input, + 'goodbuddy-windows-x64', + 'builder-debug.yml' + ), + 'tampered' + ) + + await expect( + aggregate.aggregateRelease(input, join(parent, 'upload')) + ).rejects.toThrow('完整性校验失败') + } finally { + rmSync(parent, { recursive: true, force: true }) + } + }) + it.each([ '../escape.exe', '..\\escape.exe',