feat: prepare GoodBuddy 0.8.0
This commit is contained in:
@@ -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 })
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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(() =>
|
||||
|
||||
Reference in New Issue
Block a user