Files
goodbuddy/tests/aggregate-release.test.ts
T
lofyer 4100911c34
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, macos, macos-15-intel) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Blocked by required conditions
Cross-platform packages / Validate source (push) Waiting to run
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, windows, windows-2025) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Blocked by required conditions
Cross-platform packages / Publish GitHub Release (push) Blocked by required conditions
chore: prepare GoodBuddy 0.8.3
2026-08-07 01:56:32 +08:00

266 lines
7.4 KiB
TypeScript

import {
createHash
} from 'node:crypto'
import {
mkdtempSync,
mkdirSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync
} from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
interface TargetDefinition {
platform: 'windows' | 'macos' | 'linux'
arch: 'x64' | 'arm64'
formats: string[]
}
interface AggregateModule {
aggregateRelease: (
inputDirectory: string,
outputDirectory: string
) => Promise<{
version: string
targets: Array<{
platform: string
arch: string
manifest: string
}>
}>
assertSafeName: (name: string, description: string) => void
targetDefinitions: TargetDefinition[]
}
const require = createRequire(import.meta.url)
const aggregate = require(
'../build/aggregate-release.cjs'
) as AggregateModule
const packageVersion = (
require('../package.json') as { version: string }
).version
function sha256(value: string): string {
return createHash('sha256').update(value).digest('hex')
}
function artifactName(
target: TargetDefinition,
format: string
): string {
const base =
`GoodBuddy-${packageVersion}-${target.platform}-${target.arch}`
if (format === 'nsis') {
return `${base}-setup.exe`
}
if (format === 'portable') {
return `${base}-portable.zip`
}
const extension = format === 'zip' ? 'zip' : format
return `${base}.${extension}`
}
function createDownloadedArtifacts(parent: string): string {
const input = join(parent, 'downloads')
mkdirSync(input)
for (const target of aggregate.targetDefinitions) {
const key = `${target.platform}-${target.arch}`
const directory = join(input, `goodbuddy-${key}`)
mkdirSync(directory)
const files = target.formats.map((format) => {
const name = artifactName(target, format)
const content = `${key}:${format}`
writeFileSync(join(directory, name), content)
return {
name,
size: Buffer.byteLength(content),
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)
})
const metadataName = `metadata-${key}.json`
const metadataContent = `${key}:metadata`
writeFileSync(
join(directory, metadataName),
metadataContent
)
files.push({
name: metadataName,
size: Buffer.byteLength(metadataContent),
sha256: sha256(metadataContent)
})
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({
formatVersion: 1,
productName: 'GoodBuddy',
version: packageVersion,
platform: target.platform,
arch: target.arch,
formats: target.formats,
files
}, null, 2)}\n`
)
}
return input
}
describe('release asset aggregation', () => {
it('strictly verifies six targets and writes isolated upload assets', async () => {
const parent = mkdtempSync(
join(tmpdir(), 'goodbuddy-release-aggregate-')
)
try {
const input = createDownloadedArtifacts(parent)
const output = join(parent, 'upload')
const manifest = await aggregate.aggregateRelease(input, output)
expect(manifest.version).toBe(packageVersion)
expect(manifest.targets).toHaveLength(6)
expect(
manifest.targets.map((target) => target.manifest)
).toEqual([
'release-manifest-windows-x64.json',
'release-manifest-windows-arm64.json',
'release-manifest-macos-x64.json',
'release-manifest-macos-arm64.json',
'release-manifest-linux-x64.json',
'release-manifest-linux-arm64.json'
])
const outputNames = readdirSync(output)
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.startsWith('metadata-'))
).toBe(false)
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'
)
expect(sums.trim().split('\n')).toHaveLength(19)
expect(sums).toContain(
'release-manifest-windows-x64.json'
)
expect(sums).not.toMatch(/\sSHA256SUMS(?:\r?\n|$)/u)
} finally {
rmSync(parent, { recursive: true, force: true })
}
})
it('rejects package hash mismatches', async () => {
const parent = mkdtempSync(
join(tmpdir(), 'goodbuddy-release-hash-')
)
try {
const input = createDownloadedArtifacts(parent)
const file = join(
input,
'goodbuddy-windows-x64',
artifactName(aggregate.targetDefinitions[0]!, 'nsis')
)
writeFileSync(file, 'tampered')
await expect(
aggregate.aggregateRelease(input, join(parent, 'upload'))
).rejects.toThrow('完整性校验失败')
} finally {
rmSync(parent, { recursive: true, force: true })
}
})
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',
'/tmp/escape.exe',
'nested/file.exe'
])('rejects path traversal in file name %s', (name) => {
expect(() =>
aggregate.assertSafeName(name, '测试文件名')
).toThrow('不安全路径')
})
it('rejects undeclared files', async () => {
const parent = mkdtempSync(
join(tmpdir(), 'goodbuddy-release-extra-')
)
try {
const input = createDownloadedArtifacts(parent)
writeFileSync(
join(input, 'goodbuddy-linux-x64', 'unknown.rpm'),
'unknown'
)
await expect(
aggregate.aggregateRelease(input, join(parent, 'upload'))
).rejects.toThrow('未声明的文件')
} finally {
rmSync(parent, { recursive: true, force: true })
}
})
})