feat: prepare GoodBuddy 0.8.0

This commit is contained in:
lofyer
2026-08-05 19:40:40 +08:00
parent 38ac2206f2
commit 39a457ded8
96 changed files with 14221 additions and 1027 deletions
+190
View File
@@ -0,0 +1,190 @@
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.exe`
}
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)
}
})
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')
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.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 })
}
})
})
+6 -3
View File
@@ -58,6 +58,9 @@ interface ReleaseBuilderModule {
}
const require = createRequire(import.meta.url)
const packageVersion = (
require('../package.json') as { version: string }
).version
const releaseBuilder = require(
'../build/build-release.cjs'
) as ReleaseBuilderModule
@@ -297,14 +300,14 @@ describe('release output safety', () => {
writeFileSync(
join(
directory,
'GoodBuddy-0.1.0-windows-x64-setup.exe'
`GoodBuddy-${packageVersion}-windows-x64-setup.exe`
),
'MZ'
)
writeFileSync(
join(
directory,
'GoodBuddy-0.1.0-windows-x64-portable.exe'
`GoodBuddy-${packageVersion}-windows-x64-portable.exe`
),
'MZ'
)
@@ -317,7 +320,7 @@ describe('release output safety', () => {
rmSync(
join(
directory,
'GoodBuddy-0.1.0-windows-x64-portable.exe'
`GoodBuddy-${packageVersion}-windows-x64-portable.exe`
)
)
expect(() =>